-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
certificate: prefer natural language terms vs code names
- Loading branch information
1 parent
25b4b66
commit 7fa14be
Showing
12 changed files
with
558 additions
and
393 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export * from './certificate/format.js'; | ||
export * from './certificate/ecPrivateKey.js'; | ||
export * from './certificate/rsaPrivateKey.js'; | ||
export * from './certificate/privateKeyInformation.js'; | ||
export * from './certificate/csr.js'; | ||
export * from './certificate/publicKey.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { extractPublicJWK, parseAlgorithmIdentifier } from '../../lib/jwa.js'; | ||
import { | ||
ASN_CLASS, ASN_CONSTRUCTED, | ||
encodeAttribute, encodeBitString, encodeDER, encodeDNSName, encodeExtension, | ||
encodeExtensions, encodeInteger, encodeObjectIdentifier, encodePrintableString, | ||
encodeSequence, encodeSet, encodeSubjectAltNameValue, encodeUTF8String, | ||
parseSignatureAlgorithm, | ||
} from '../asn1.js'; | ||
import { importJWK, sign } from '../crypto.js'; | ||
|
||
import { derFromPEM, pemFromDER } from './format.js'; | ||
import { derFromSPKI, spkiFromJWK } from './publicKey.js'; | ||
|
||
const CSR_HEADER = '-----BEGIN CERTIFICATE REQUEST-----'; | ||
const CSR_FOOTER = '-----END CERTIFICATE REQUEST-----'; | ||
|
||
/** | ||
* @param {string|Uint8Array} csr | ||
* @return {Uint8Array} | ||
*/ | ||
export function derFromCSR(csr) { | ||
if (typeof csr !== 'string') return csr; | ||
return derFromPEM(csr, CSR_HEADER, CSR_FOOTER); | ||
} | ||
|
||
/** | ||
* @param {string|Uint8Array} csr | ||
* @return {string} | ||
*/ | ||
export function pemFromCSR(csr) { | ||
return pemFromDER(derFromCSR(csr), CSR_HEADER, CSR_FOOTER); | ||
} | ||
|
||
/** | ||
* @see https://datatracker.ietf.org/doc/html/rfc2986 | ||
* @see https://datatracker.ietf.org/doc/html/rfc2985#section-5.4 (Attributes) | ||
* @see https://www.rfc-editor.org/rfc/rfc5280#appendix-A.1 (Extensions) | ||
* @param {Object} options | ||
* @param {string} options.commonName | ||
* @param {string} [options.organizationName] | ||
* @param {string} [options.organizationalUnitName] | ||
* @param {string} [options.localityName] | ||
* @param {string} [options.stateOrProvinceName] | ||
* @param {string} [options.countryName] | ||
* @param {string[]} [options.altNames] | ||
* @param {JWK} options.jwk | ||
* @return {Promise<Uint8Array>} | ||
*/ | ||
export async function createCSR(options) { | ||
/** @type {[string,string,(input: string) => number[]][]} */ | ||
const oidMappings = [ | ||
['2.5.4.3', options.commonName, encodeUTF8String], | ||
['2.5.4.6', options.countryName, encodePrintableString], | ||
['2.5.4.7', options.localityName, encodeUTF8String], | ||
['2.5.4.8', options.stateOrProvinceName, encodeUTF8String], | ||
['2.5.4.10', options.organizationName, encodeUTF8String], | ||
['2.5.4.11', options.organizationalUnitName, encodeUTF8String], | ||
]; | ||
// dn are composed as sets | ||
const dnSets = oidMappings | ||
.filter(([oid, value]) => value) | ||
.map(([oid, value, encoder]) => encodeSet( | ||
encodeSequence( | ||
encodeObjectIdentifier(oid), | ||
encoder(value), | ||
), | ||
)); | ||
const subject = encodeSequence(...dnSets); | ||
|
||
const algorithmIdentifier = parseAlgorithmIdentifier(options.jwk); | ||
|
||
// Note: Could use ASN1 to compile SPKI, but using platform implementation is safer | ||
const spki = await spkiFromJWK(extractPublicJWK(options.jwk), algorithmIdentifier); | ||
const SubjectPKInfo = derFromSPKI(spki); | ||
|
||
let attributes; | ||
if (options.altNames?.length) { | ||
const extensionRequestAttributeOID = '1.2.840.113549.1.9.14'; | ||
const subjectAltNameOID = '2.5.29.17'; | ||
|
||
const attributesTag = 0; | ||
attributes = encodeDER( | ||
// eslint-disable-next-line no-bitwise | ||
ASN_CLASS.CONTEXT_SPECIFIC | ASN_CONSTRUCTED | attributesTag, | ||
encodeAttribute( | ||
extensionRequestAttributeOID, | ||
encodeExtensions( | ||
encodeExtension( | ||
subjectAltNameOID, | ||
encodeSubjectAltNameValue( | ||
...options.altNames.map((dnsName) => encodeDNSName(dnsName)), | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
|
||
const certificationRequestInfo = encodeSequence( | ||
encodeInteger(0), // Version | ||
subject, // Subject | ||
SubjectPKInfo, // SubjectPKInfo | ||
attributes ?? [], // Attributes, | ||
); | ||
|
||
const key = await importJWK({ ...options.jwk, key_ops: ['sign'] }, algorithmIdentifier); | ||
const signedCRI = await sign(algorithmIdentifier, key, Uint8Array.from(certificationRequestInfo)); | ||
const dataArray = new Uint8Array(signedCRI); | ||
const signature = encodeBitString(dataArray, signedCRI.byteLength); | ||
const certificationRequest = encodeSequence( | ||
certificationRequestInfo, | ||
parseSignatureAlgorithm(options.jwk), | ||
signature, | ||
); | ||
return Uint8Array.from(certificationRequest); | ||
} | ||
|
||
/** @alias */ | ||
export const createPKCS10 = createCSR; | ||
/** @alias */ | ||
export const derFromPKCS10 = derFromCSR; | ||
/** @alias */ | ||
export const pemFromPKCS10 = pemFromCSR; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { derFromPEM, pemFromDER } from './format.js'; | ||
|
||
// https://datatracker.ietf.org/doc/html/rfc5915 | ||
const EC_PRIVATE_KEY_HEADER = '-----BEGIN EC PRIVATE KEY-----'; | ||
const EC_PRIVATE_KEY_FOOTER = '-----END EC PRIVATE KEY-----'; | ||
|
||
/** | ||
* @param {string|Uint8Array} ecPrivateKey | ||
* @return {Uint8Array} | ||
*/ | ||
export function derFromECPrivateKey(ecPrivateKey) { | ||
if (typeof ecPrivateKey !== 'string') return ecPrivateKey; | ||
return derFromPEM(ecPrivateKey, EC_PRIVATE_KEY_HEADER, EC_PRIVATE_KEY_FOOTER); | ||
} | ||
|
||
/** | ||
* @param {string|Uint8Array} ecPrivateKey | ||
* @return {string} | ||
*/ | ||
export function pemFromECPrivateKey(ecPrivateKey) { | ||
return pemFromDER(derFromECPrivateKey(ecPrivateKey), EC_PRIVATE_KEY_HEADER, EC_PRIVATE_KEY_FOOTER); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { decodeBase64AsArray, encodeBase64AsString } from '../base64.js'; | ||
|
||
/** | ||
* @param {string} pem | ||
* @param {string} [header] | ||
* @param {string} [footer] | ||
* @return {Uint8Array} | ||
*/ | ||
export function derFromPEM(pem, header, footer) { | ||
let content; | ||
if (!header || !footer) { | ||
const splits = pem.split('-----'); | ||
if (splits.length === 1) { | ||
// No header, assume encoded DER | ||
content = pem; | ||
} else if (splits.length === 5) { | ||
content = splits[2]; | ||
} else { | ||
throw new Error('Invalid PEM'); | ||
} | ||
} else { | ||
const indexOfHeader = pem.indexOf(header); | ||
const indexOfFooter = pem.indexOf(footer); | ||
if (indexOfHeader !== -1 && indexOfFooter !== -1) { | ||
content = pem.slice( | ||
pem.indexOf(header) + header.length, | ||
pem.indexOf(footer), | ||
); | ||
} else if (indexOfHeader === -1 && indexOfHeader === -1) { | ||
// No header, assume encoded DER | ||
content = pem; | ||
} else { | ||
throw new Error('Invalid PEM!'); | ||
} | ||
} | ||
return decodeBase64AsArray(content.replaceAll(/\s/g, '')); | ||
} | ||
|
||
/** | ||
* @param {Uint8Array} der | ||
* @param {string} header | ||
* @param {string} footer | ||
*/ | ||
export function pemFromDER(der, header, footer) { | ||
return `${header}\n${encodeBase64AsString(der).replaceAll(/(.{64})/g, '$1\n')}\n${footer}`; | ||
} | ||
|
||
/** | ||
* Reconstructs PEM with line breaks and 64 char limit | ||
* @param {string} pem | ||
* @param {string} [header] | ||
* @param {string} [footer] | ||
* @return {string} | ||
*/ | ||
export function formatPEM(pem, header, footer) { | ||
let content; | ||
if (!header || !footer) { | ||
const splits = pem.split('-----'); | ||
if (splits.length !== 5) throw new Error('Invalid PEM'); | ||
|
||
header = `-----${splits[1]}-----`; | ||
content = splits[2]; | ||
footer = `-----${splits[3]}-----`; | ||
} else { | ||
const indexOfHeader = pem.indexOf(header); | ||
const indexOfFooter = pem.indexOf(footer); | ||
|
||
if (indexOfHeader !== -1 && indexOfFooter !== -1) { | ||
content = pem.slice( | ||
pem.indexOf(header) + header.length, | ||
pem.indexOf(footer), | ||
); | ||
} else if (indexOfHeader === -1 && indexOfHeader === -1) { | ||
// No header, assume encoded DER | ||
content = pem; | ||
} else { | ||
throw new Error('Invalid PEM!'); | ||
} | ||
} | ||
return `${header}\n${content.replaceAll(/\s/g, '').replaceAll(/(.{64})/g, '$1\n')}\n${footer}`; | ||
} |
Oops, something went wrong.