From 7a6132eddf3bdaf4b51aa8b9cbf788b2919eb19c Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Fri, 24 Nov 2023 22:57:27 +0000 Subject: [PATCH] Add helpers for `parseCertificate` and `parseCertificateBundle` Co-Authored-By: Alper Dedeoglu <39748566+alperdedeoglu@users.noreply.github.com> --- package.json | 2 + pnpm-lock.yaml | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 78 ++++++++++++++++++++ 3 files changed, 271 insertions(+) diff --git a/package.json b/package.json index 6f83ab2..3af786d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ }, "dependencies": { "@grpc/grpc-js": "^1.9.11", + "@peculiar/webcrypto": "^1.4.3", + "@peculiar/x509": "^1.9.5", "@protobuf-ts/grpc-transport": "^2.9.1", "@protobuf-ts/runtime": "^2.9.1", "@protobuf-ts/runtime-rpc": "^2.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cfb618..bf9ada4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ dependencies: '@grpc/grpc-js': specifier: ^1.9.11 version: 1.9.11 + '@peculiar/webcrypto': + specifier: ^1.4.3 + version: 1.4.3 + '@peculiar/x509': + specifier: ^1.9.5 + version: 1.9.5 '@protobuf-ts/grpc-transport': specifier: ^2.9.1 version: 2.9.1(@grpc/grpc-js@1.9.11) @@ -609,6 +615,137 @@ packages: fastq: 1.15.0 dev: true + /@peculiar/asn1-cms@2.3.8: + resolution: {integrity: sha512-Wtk9R7yQxGaIaawHorWKP2OOOm/RZzamOmSWwaqGphIuU6TcKYih0slL6asZlSSZtVoYTrBfrddSOD/jTu9vuQ==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + '@peculiar/asn1-x509-attr': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-csr@2.3.8: + resolution: {integrity: sha512-ZmAaP2hfzgIGdMLcot8gHTykzoI+X/S53x1xoGbTmratETIaAbSWMiPGvZmXRA0SNEIydpMkzYtq4fQBxN1u1w==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-ecc@2.3.8: + resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-pfx@2.3.8: + resolution: {integrity: sha512-XhdnCVznMmSmgy68B9pVxiZ1XkKoE1BjO4Hv+eUGiY1pM14msLsFZ3N7K46SoITIVZLq92kKkXpGiTfRjlNLyg==} + dependencies: + '@peculiar/asn1-cms': 2.3.8 + '@peculiar/asn1-pkcs8': 2.3.8 + '@peculiar/asn1-rsa': 2.3.8 + '@peculiar/asn1-schema': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-pkcs8@2.3.8: + resolution: {integrity: sha512-rL8k2x59v8lZiwLRqdMMmOJ30GHt6yuHISFIuuWivWjAJjnxzZBVzMTQ72sknX5MeTSSvGwPmEFk2/N8+UztFQ==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-pkcs9@2.3.8: + resolution: {integrity: sha512-+nONq5tcK7vm3qdY7ZKoSQGQjhJYMJbwJGbXLFOhmqsFIxEWyQPHyV99+wshOjpOjg0wUSSkEEzX2hx5P6EKeQ==} + dependencies: + '@peculiar/asn1-cms': 2.3.8 + '@peculiar/asn1-pfx': 2.3.8 + '@peculiar/asn1-pkcs8': 2.3.8 + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + '@peculiar/asn1-x509-attr': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-rsa@2.3.8: + resolution: {integrity: sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-schema@2.3.8: + resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-x509-attr@2.3.8: + resolution: {integrity: sha512-4Z8mSN95MOuX04Aku9BUyMdsMKtVQUqWnr627IheiWnwFoheUhX3R4Y2zh23M7m80r4/WG8MOAckRKc77IRv6g==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.2 + dev: false + + /@peculiar/asn1-x509@2.3.8: + resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + asn1js: 3.0.5 + ipaddr.js: 2.1.0 + pvtsutils: 1.3.5 + tslib: 2.6.2 + dev: false + + /@peculiar/json-schema@1.1.12: + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@peculiar/webcrypto@1.4.3: + resolution: {integrity: sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==} + engines: {node: '>=10.12.0'} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/json-schema': 1.1.12 + pvtsutils: 1.3.5 + tslib: 2.6.2 + webcrypto-core: 1.7.7 + dev: false + + /@peculiar/x509@1.9.5: + resolution: {integrity: sha512-6HBrlgoyH8sod0PTjQ8hzOL4/f5L94s5lwiL9Gr0P5HiSO8eeNgKoiB+s7VhDczE2aaloAgDXFjoQHVEcTg4mg==} + dependencies: + '@peculiar/asn1-cms': 2.3.8 + '@peculiar/asn1-csr': 2.3.8 + '@peculiar/asn1-ecc': 2.3.8 + '@peculiar/asn1-pkcs9': 2.3.8 + '@peculiar/asn1-rsa': 2.3.8 + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + pvtsutils: 1.3.5 + reflect-metadata: 0.1.13 + tslib: 2.6.2 + tsyringe: 4.8.0 + dev: false + /@protobuf-ts/grpc-transport@2.9.1(@grpc/grpc-js@1.9.11): resolution: {integrity: sha512-p3o69oQUqMX1dG0QcBsnK7/2h0ReEIfJRbZykMCumTn2uAc9znTfh74xB8aH8I5Q+sWphucG8mPytJ/QIW9WSA==} peerDependencies: @@ -835,6 +972,15 @@ packages: engines: {node: '>=8'} dev: true + /asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.2 + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true @@ -1132,6 +1278,11 @@ packages: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} dev: true + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1368,6 +1519,17 @@ packages: engines: {node: '>=6'} dev: true + /pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + dependencies: + tslib: 2.6.2 + dev: false + + /pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -1379,6 +1541,10 @@ packages: picomatch: 2.3.1 dev: true + /reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1522,6 +1688,14 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsup@8.0.1(typescript@5.3.2): resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} engines: {node: '>=18'} @@ -1572,6 +1746,13 @@ packages: fsevents: 2.3.3 dev: true + /tsyringe@4.8.0: + resolution: {integrity: sha512-YB1FG+axdxADa3ncEtRnQCFq/M0lALGLxSZeVNbTU8NqhOVc51nnv2CISTcvc1kyv6EGPtXVr0v6lWeDxiijOA==} + engines: {node: '>= 6.0.0'} + dependencies: + tslib: 1.14.1 + dev: false + /typescript@3.9.10: resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} engines: {node: '>=4.2.0'} @@ -1600,6 +1781,16 @@ packages: '@fastify/busboy': 2.0.0 dev: true + /webcrypto-core@1.7.7: + resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/json-schema': 1.1.12 + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.2 + dev: false + /webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true diff --git a/src/index.ts b/src/index.ts index e26dea9..d8fced4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import {ChannelCredentials} from '@grpc/grpc-js' +import {Crypto} from '@peculiar/webcrypto' +import * as x509 from '@peculiar/x509' import {GrpcTransport} from '@protobuf-ts/grpc-transport' import {SpiffeWorkloadAPIClient} from './proto/spiffe/workload/workload.client' @@ -16,6 +18,82 @@ export function createClient(baseURL?: string) { return new SpiffeWorkloadAPIClient(transport) } +/** + * Utility function to parse a raw certificate. Can be used to convert the + * raw certificate into a PEM certificate: + * + * ```typescript + * const cert = parseCertificate(data) + * const pem = cert.toString('pem') + * ``` + * + * **Note:** if you have a certificate bundle, use `parseCertificateBundle` instead, as + * this function will incorrectly parse bundles as a single certificate. + * + * @param data The raw data of the certificate + * @returns [X509Certificate](https://peculiarventures.github.io/x509/classes/X509Certificate.html) + * @see https://github.com/PeculiarVentures/x509 + */ +export function parseCertificate(data: Uint8Array): x509.X509Certificate { + initCryptoProvider() + + return new x509.X509Certificate(data) +} + +/** + * Utility function to parse a certificate bundle. Can be used to convert the + * bundle into a PEM chain: + * + * ```typescript + * const certs = parseCertificateBundle(data) + * const pemChain = certs.toString('pem-chain') + * ``` + * + * @param data The raw data of the certificate bundle + * @returns [X509Certificates](https://peculiarventures.github.io/x509/classes/X509Certificates.html) + * @see https://github.com/PeculiarVentures/x509 + */ +export function parseCertificateBundle(data: Uint8Array): x509.X509Certificates { + initCryptoProvider() + + let currentIndex = 0 + const certs: x509.X509Certificate[] = [] + + while (currentIndex < data.length) { + const beginOfSequence = currentIndex + + // Skip the first tag byte + currentIndex += 1 + + // Get the second byte for length value representing bytes + let length = data[currentIndex++] + if (length === 0x80) throw new TypeError('Indefinite length encoding not supported') + if (length & 0x80) { + const lengthBytes = length & 0x7f + length = 0 + for (let i = 0; i < lengthBytes; i++) { + length = (length << 8) | data[currentIndex++] + } + } + + certs.push(new x509.X509Certificate(data.slice(beginOfSequence, currentIndex + length))) + + // Move the current index to the next tag + currentIndex += length + } + + return new x509.X509Certificates(certs) +} + +function initCryptoProvider() { + try { + x509.cryptoProvider.get() + } catch { + const crypto = new Crypto() + x509.cryptoProvider.set(crypto) + } +} + export * from './proto/google/protobuf/struct' export * from './proto/spiffe/workload/workload' export * from './proto/spiffe/workload/workload.client'