Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix PrintableString for Name #71

Merged
merged 6 commits into from Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 13 additions & 8 deletions .github/workflows/test.yml
Expand Up @@ -39,13 +39,18 @@ jobs:
- name: Run test with coverage
run: npm run coverage

# Fixes problem with incorrect SF paths. See https://github.com/coverallsapp/github-action/issues/125
- name: Update lcov.info
run: |
sed -E "s/SF:(.+file:(.+))/SF:\2/g" ./coverage/lcov.info > coverage/lcov.new.info
mv ./coverage/lcov.new.info ./coverage/lcov.info

- name: Coveralls
uses: coverallsapp/github-action@master
uses: coverallsapp/github-action@v1
with:
parallel: true
flag-name: run-${{ matrix.node-version }}

finish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Close parallel build
uses: coverallsapp/github-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
carryforward: "run-20.x"
10 changes: 8 additions & 2 deletions rollup.config.mjs
Expand Up @@ -33,7 +33,10 @@ const main = {
tsconfigOverride: {
compilerOptions: {
module: "ES2015",
}
},
exclude: [
"test",
],
},
}),
],
Expand Down Expand Up @@ -67,7 +70,10 @@ const browser = [
tsconfigOverride: {
compilerOptions: {
module: "es2015",
}
},
exclude: [
"test",
],
}
}),
],
Expand Down
16 changes: 14 additions & 2 deletions src/name.ts
Expand Up @@ -121,6 +121,17 @@ export class Name {
return true;
}

/**
* Checks if a given string is a printable string.
* A printable string contains only printable ASCII characters.
*
* @param text - The string to be checked.
* @returns True if the string is a printable string, false otherwise.
*/
public static isPrintableString(text: string): boolean {
return /^[A-Za-z0-9 '()+,-./:=?]*$/g.test(text);
}

/**
* ASN.1 Name
*/
Expand Down Expand Up @@ -275,10 +286,11 @@ export class Name {
if (type === this.getName("E") || type === this.getName("DC")) {
attr.value.ia5String = value;
} else {
// Use Utf8String for non ASCII strings
if (Name.isASCII(value)) {
if (Name.isPrintableString(value)) {
// PrintableString
attr.value.printableString = value;
} else {
// UTF8String
attr.value.utf8String = value;
}
}
Expand Down
40 changes: 27 additions & 13 deletions src/x509_cert_generator.ts
Expand Up @@ -18,25 +18,25 @@ export type X509CertificateCreateParamsName = string | JsonName | Name;
*/
export interface X509CertificateCreateParamsBase {
/**
* Hexadecimal serial number
* Hexadecimal serial number. If not specified, random value will be generated
*/
serialNumber: string;
serialNumber?: string;
/**
* Date before which certificate can't be used
* Date before which certificate can't be used. Default is current date
*/
notBefore: Date;
notBefore?: Date;
/**
* Date after which certificate can't be used
* Date after which certificate can't be used. Default is 1 year from now
*/
notAfter: Date;
notAfter?: Date;
/**
* List of extensions
*/
extensions?: Extension[];
/**
* Signing algorithm
* Signing algorithm. Default is SHA-256 with key algorithm
*/
signingAlgorithm: Algorithm | EcdsaParams;
signingAlgorithm?: Algorithm | EcdsaParams;
}

/**
Expand Down Expand Up @@ -96,7 +96,7 @@ export class X509CertificateGenerator {
throw new Error("Bad field 'keys' in 'params' argument. 'privateKey' is empty");
}
if (!params.keys.publicKey) {
throw new Error("Bad field 'keys' in 'params' argument. 'privateKey' is empty");
throw new Error("Bad field 'keys' in 'params' argument. 'publicKey' is empty");
}

return this.create({
Expand Down Expand Up @@ -129,13 +129,24 @@ export class X509CertificateGenerator {
spki = await crypto.subtle.exportKey("spki", params.publicKey);
}

// SerialNumber must be positive integer
const serialNumber = params.serialNumber
? BufferSourceConverter.toUint8Array(Convert.FromHex(params.serialNumber))
: crypto.getRandomValues(new Uint8Array(16));
if (serialNumber[0] > 0x7F) {
serialNumber[0] &= 0x7F;
}

const notBefore = params.notBefore || new Date();
const notAfter = params.notAfter || new Date(notBefore.getTime() + 31536000000); // 1 year

const asnX509 = new asn1X509.Certificate({
tbsCertificate: new asn1X509.TBSCertificate({
version: asn1X509.Version.v3,
serialNumber: Convert.FromHex(params.serialNumber),
serialNumber: serialNumber,
validity: new asn1X509.Validity({
notBefore: params.notBefore,
notAfter: params.notAfter,
notBefore,
notAfter,
}),
extensions: new asn1X509.Extensions(params.extensions?.map(o => AsnConvert.parse(o.rawData, asn1X509.Extension)) || []),
subjectPublicKeyInfo: AsnConvert.parse(spki, asn1X509.SubjectPublicKeyInfo),
Expand All @@ -155,8 +166,11 @@ export class X509CertificateGenerator {
}

// Set signing algorithm
const defaultSigningAlgorithm = {
hash: "SHA-256",
};
const signatureAlgorithm = ("signingKey" in params)
? { ...params.signingAlgorithm, ...params.signingKey.algorithm } as HashedAlgorithm
? { ...defaultSigningAlgorithm, ...params.signingAlgorithm, ...params.signingKey.algorithm } as HashedAlgorithm
: params.publicKey.algorithm as HashedAlgorithm;

const algProv = container.resolve<AlgorithmProvider>(diAlgorithmProvider);
Expand Down
14 changes: 13 additions & 1 deletion test/name.ts
Expand Up @@ -125,7 +125,7 @@ context("Name", () => {
"GUID": "1.2.3.4.5.3",
});

assert.strictEqual(Convert.ToHex(name.toArrayBuffer()), "30663119301706052a03040501130e736f6d6540656d61696c2e636f6d3116301406052a03040502130b3139322e3136382e302e313131302f06052a0304050313267b38656531336535332d326331632d343262622d386466372d3339393237633062646262367d");
assert.strictEqual(Convert.ToHex(name.toArrayBuffer()), "30663119301706052a030405010c0e736f6d6540656d61696c2e636f6d3116301406052a03040502130b3139322e3136382e302e313131302f06052a030405030c267b38656531336535332d326331632d343262622d386466372d3339393237633062646262367d");
assert.deepStrictEqual(name.toJSON(), [
{ "Email": ["some@email.com"] },
{ "IP": ["192.168.0.1"] },
Expand Down Expand Up @@ -247,4 +247,16 @@ context("NameIdentifier", () => {

});

context("isASCII", () => {
it("should return true for ASCII text", () => {
const result = x509.Name.isASCII("Hello, World!");
assert.strictEqual(result, true);
});

it("should return false for non-ASCII text", () => {
const result = x509.Name.isASCII("Привет, мир!");
assert.strictEqual(result, false);
});
});

});
68 changes: 66 additions & 2 deletions test/test_x509_cert_generator.ts
Expand Up @@ -167,10 +167,74 @@ function testCertPreSigned(testEntry: any) {
});
}


describe(path.basename(__filename), () => {
theTestX509CertificateGeneratorVector.forEach(testEntry => {
testCertSelfSign(testEntry);
testCertPreSigned(testEntry);
});
});

context("create", () => {
let keys: CryptoKeyPair;

before(async () => {
keys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
});

it("should handle PublicKey instance", async () => {
const spki = await crypto.subtle.exportKey("spki", keys.publicKey);
const publicKey = new x509.PublicKey(spki);
const cert = await x509.X509CertificateGenerator.create({
publicKey,
signingKey: keys.privateKey,
});
assert.ok(cert);
});

it("should handle object with publicKey property", async () => {
const certWithPublicKey = await x509.X509CertificateGenerator.create({
publicKey: keys.publicKey,
signingKey: keys.privateKey,
});
const cert = await x509.X509CertificateGenerator.create({
publicKey: certWithPublicKey,
signingKey: keys.privateKey,
});
assert.ok(cert);
});

it("should handle BufferSource publicKey", async () => {
const spki = await crypto.subtle.exportKey("spki", keys.publicKey);
const cert = await x509.X509CertificateGenerator.create({
publicKey: spki,
signingKey: keys.privateKey,
});
assert.ok(cert);
});
});

context("createSelfSigned", () => {
it("should throw an error if privateKey is empty", async () => {
await assert.rejects(
x509.X509CertificateGenerator.createSelfSigned({
keys: { privateKey: null, publicKey: {} } as any,
// other parameters...
}),
{
message: "Bad field 'keys' in 'params' argument. 'privateKey' is empty"
}
);
});

it("should throw an error if publicKey is empty", async () => {
await assert.rejects(
x509.X509CertificateGenerator.createSelfSigned({
keys: { privateKey: {}, publicKey: null } as any,
// other parameters...
}),
{
message: "Bad field 'keys' in 'params' argument. 'publicKey' is empty"
}
);
});
});
});