UNPKG

@sphereon/ssi-sdk-ext.x509-utils

Version:

Sphereon SSI-SDK plugin functions for X.509 Certificate handling.

188 lines (167 loc) 6.46 kB
import { X509Certificate } from '@peculiar/x509' import { Certificate } from 'pkijs' // @ts-ignore import * as u8a from 'uint8arrays' const { fromString, toString } = u8a // @ts-ignore import keyto from '@trust/keyto' import type { KeyVisibility } from '../types' import type { JsonWebKey } from '@sphereon/ssi-types' // Based on (MIT licensed): // https://github.com/hildjj/node-posh/blob/master/lib/index.js export function pemCertChainTox5c(cert: string, maxDepth?: number): string[] { if (!maxDepth) { maxDepth = 0 } /* * Convert a PEM-encoded certificate to the version used in the x5c element * of a [JSON Web Key](http://tools.ietf.org/html/draft-ietf-jose-json-web-key). * * `cert` PEM-encoded certificate chain * `maxdepth` The maximum number of certificates to use from the chain. */ const intermediate = cert .replace(/-----[^\n]+\n?/gm, ',') .replace(/\n/g, '') .replace(/\r/g, '') let x5c = intermediate.split(',').filter(function (c) { return c.length > 0 }) if (maxDepth > 0) { x5c = x5c.splice(0, maxDepth) } return x5c } export function x5cToPemCertChain(x5c: string[], maxDepth?: number): string { if (!maxDepth) { maxDepth = 0 } const length = maxDepth === 0 ? x5c.length : Math.min(maxDepth, x5c.length) let pem = '' for (let i = 0; i < length; i++) { pem += derToPEM(x5c[i], 'CERTIFICATE') } return pem } export const pemOrDerToX509Certificate = (cert: string | Uint8Array | X509Certificate): Certificate => { let DER: string | undefined = typeof cert === 'string' ? cert : undefined if (typeof cert === 'object' && !(cert instanceof Uint8Array)) { // X509Certificate object return Certificate.fromBER(cert.rawData) } else if (typeof cert !== 'string') { return Certificate.fromBER(cert) } else if (cert.includes('CERTIFICATE')) { DER = PEMToDer(cert) } if (!DER) { throw Error('Invalid cert input value supplied. PEM, DER, Bytes and X509Certificate object are supported') } return Certificate.fromBER(fromString(DER, 'base64pad')) } export const areCertificatesEqual = (cert1: Certificate, cert2: Certificate): boolean => { return cert1.signatureValue.isEqual(cert2.signatureValue) } export const toKeyObject = (PEM: string, visibility: KeyVisibility = 'public') => { const jwk = PEMToJwk(PEM, visibility) const keyVisibility: KeyVisibility = jwk.d ? 'private' : 'public' const keyHex = keyVisibility === 'private' ? privateKeyHexFromPEM(PEM) : publicKeyHexFromPEM(PEM) return { pem: hexToPEM(keyHex, visibility), jwk, keyHex, keyType: keyVisibility, } } export const jwkToPEM = (jwk: JsonWebKey, visibility: KeyVisibility = 'public'): string => { return keyto.from(jwk, 'jwk').toString('pem', visibility === 'public' ? 'public_pkcs8' : 'private_pkcs8') } export const PEMToJwk = (pem: string, visibility: KeyVisibility = 'public'): JsonWebKey => { return keyto.from(pem, 'pem').toJwk(visibility) } export const privateKeyHexFromPEM = (PEM: string) => { return PEMToHex(PEM) } export const hexKeyFromPEMBasedJwk = (jwk: JsonWebKey, visibility: KeyVisibility = 'public'): string => { if (visibility === 'private') { return privateKeyHexFromPEM(jwkToPEM(jwk, 'private')) } else { return publicKeyHexFromPEM(jwkToPEM(jwk, 'public')) } } export const publicKeyHexFromPEM = (PEM: string) => { const hex = PEMToHex(PEM) if (PEM.includes('CERTIFICATE')) { throw Error('Cannot directly deduce public Key from PEM Certificate yet') } else if (!PEM.includes('PRIVATE')) { return hex } const publicJwk = PEMToJwk(PEM, 'public') const publicPEM = jwkToPEM(publicJwk, 'public') return PEMToHex(publicPEM) } export const PEMToHex = (PEM: string, headerKey?: string): string => { if (PEM.indexOf('-----BEGIN ') == -1) { throw Error(`PEM header not found: ${headerKey}`) } let strippedPem: string if (headerKey) { strippedPem = PEM.replace(new RegExp('^[^]*-----BEGIN ' + headerKey + '-----'), '') strippedPem = strippedPem.replace(new RegExp('-----END ' + headerKey + '-----[^]*$'), '') } else { strippedPem = PEM.replace(/^[^]*-----BEGIN [^-]+-----/, '') strippedPem = strippedPem.replace(/-----END [^-]+-----[^]*$/, '') } return base64ToHex(strippedPem, 'base64pad') } export function PEMToBinary(pem: string): Uint8Array { const pemContents = pem .replace(/^[^]*-----BEGIN [^-]+-----/, '') .replace(/-----END [^-]+-----[^]*$/, '') .replace(/\s/g, '') return fromString(pemContents, 'base64pad') } /** * Converts a base64 encoded string to hex string, removing any non-base64 characters, including newlines * @param input The input in base64, with optional newlines * @param inputEncoding */ export const base64ToHex = (input: string, inputEncoding?: 'base64' | 'base64pad' | 'base64url' | 'base64urlpad') => { const base64NoNewlines = input.replace(/[^0-9A-Za-z_\-~\/+=]*/g, '') return toString(fromString(base64NoNewlines, inputEncoding ? inputEncoding : 'base64pad'), 'base16') } export const hexToBase64 = (input: number | object | string, targetEncoding?: 'base64' | 'base64pad' | 'base64url' | 'base64urlpad'): string => { let hex = typeof input === 'string' ? input : input.toString(16) if (hex.length % 2 === 1) { hex = `0${hex}` } return toString(fromString(hex, 'base16'), targetEncoding ? targetEncoding : 'base64pad') } export const hexToPEM = (hex: string, type: KeyVisibility): string => { const base64 = hexToBase64(hex, 'base64pad') const headerKey = type === 'private' ? 'RSA PRIVATE KEY' : 'PUBLIC KEY' if (type === 'private') { const pem = derToPEM(base64, headerKey) try { PEMToJwk(pem) // We only use it to test the private key return pem } catch (error) { return derToPEM(base64, 'PRIVATE KEY') } } return derToPEM(base64, headerKey) } export function PEMToDer(pem: string): string { return pem.replace(/(-----(BEGIN|END) CERTIFICATE-----|[\n\r])/g, '') } export function derToPEM(cert: string, headerKey?: 'PUBLIC KEY' | 'RSA PRIVATE KEY' | 'PRIVATE KEY' | 'CERTIFICATE'): string { const key = headerKey ?? 'CERTIFICATE' if (cert.includes(key)) { // Was already in PEM it seems return cert } const matches = cert.match(/.{1,64}/g) if (!matches) { throw Error('Invalid cert input value supplied') } return `-----BEGIN ${key}-----\n${matches.join('\n')}\n-----END ${key}-----\n` }