PeculiarVentures / PKI.js

PKI.js is a pure JavaScript library implementing the formats that are used in PKI applications (signing, encryption, certificate requests, OCSP and TSP requests/responses). It is built on WebCrypto (Web Cryptography API) and requires no plug-ins.
http://pkijs.org
Other
1.25k stars 204 forks source link

Support interoperability with "asn1-schema/x509" #372

Open gnarea opened 1 year ago

gnarea commented 1 year ago

I have an ASN.1 SEQUENCE that contains an X.509 certificate, and I'm using a custom asn1-schema schema to validate and (de)serialise the outer SEQUENCE. The certificate is eventually used to produce/verify SignedData values, so it'd be great if I could initialise a PKI.js Certificate with an asn1-schema/x509 Certificate.

Example:

import { Certificate as Asn1Certificate } from '@peculiar/asn1-x509';
import { AsnParser } from '@peculiar/asn1-schema';
import { Certificate as PkijsCertificate } from 'pkijs';

const CERT = `MIIEczCCAyegAwIBAgIIf98Hu6tQXdMwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoG
CSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgMIGOMYGLMIGIBgNVBAMegYAAOABiADIANAA0ADEA
ZgBjADEANwA2AGQANQA3ADUANQAxADIAOABmADEAZAA5ADgAYQA5ADcAZgAxAGEANAAyAGYAMQBhADQA
NwBjADgANgBhAGQAZAAwADEAZgBhADUAOAAxADcAZQA3ADgAZgA4ADkAMQA2AGYAOQBiADYAOTAeFw0y
MzAxMTMxMDQyMzNaFw0yMzAxMTQxMDQyMzNaMIGOMYGLMIGIBgNVBAMegYAAOABiADIANAA0ADEAZgBj
ADEANwA2AGQANQA3ADUANQAxADIAOABmADEAZAA5ADgAYQA5ADcAZgAxAGEANAAyAGYAMQBhADQANwBj
ADgANgBhAGQAZAAwADEAZgBhADUAOAAxADcAZQA3ADgAZgA4ADkAMQA2AGYAOQBiADYAOTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8cE5qBNshuhv429ngLM1YfSOj+n5MentUr0/4cRLWPh0Qj
98VI3oZs7cvOfSkCeQgsIOhOmNZnC0wqAwo7dv1L2lJqi3975SplDe6VrWJSr6cJYLQxwkNFT6nYwPQ/
OvbqaZUGg4K5Qv5PEPDKO+wh78YfaeDDjMEbLoz0s0sNK/8OvESxH+fbbjwP2/r+Mhiy/PwjW9+Y5//P
Qb5LtKFLdpRqmU06cs3hDZ0Z9T1Mkohr2grp4OEPshgpixpnSSxQS3Sty3P+I08TclVDGsbr54uwXMV6
yHauyZfgw7LFJU6tpumYvwPs4v/w+0B543SQK15MC2H2xAcre75W8ncCAwEAAaNrMGkwDwYDVR0TAQH/
BAUwAwIBADArBgNVHSMEJDAigCCLJEH8F21XVRKPHZipfxpC8aR8hq3QH6WBfnj4kW+baTApBgNVHQ4E
IgQgiyRB/BdtV1USjx2YqX8aQvGkfIat0B+lgX54+JFvm2kwQQYJKoZIhvcNAQEKMDSgDzANBglghkgB
ZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgA4IBAQAgIgvgz3NQw0HW0tT/
imkaQEXAPdOVMJxjojrXmDSUMlBXlHuqcrOHcQxkAsun/QGK6G7MBdLIa64D5+adEtcbudei3EZ47uYM
BjaaY4haMJzvH6MH0k+FEjDsx85kl726v6mlpHEl1l2jLo8zXaHFmldXjFD4fKv7yIf+YR2UHBtiGu16
Ebnmvx5lePwSKau841LLrxPDy1jJbBKBO+/6uDgL49Gx2X6AxlCkYTakRitsTPBjGP4lVLWaX4VC1C/G
rWTAht1wd2thbxnYsdXFArpKLyiBf02RPibEqKFmcnjXOpt1CeJldX0J7EX5sF/uz2l/NQTh11LUkcRB
l9Lu`;

test('it', () => {
  const serialisation = Buffer.from(CERT, 'base64');
  const asn1 = AsnParser.parse(serialisation, Asn1Certificate);
  const pkijs = new PkijsCertificate({ schema: asn1 });
  expect(Buffer.from(pkijs.toSchema().toBER())).toStrictEqual(serialisation);
});

As a workaround, I'm re-serialising the certificate with asn1-schema/x509 and re-deserialising it with PKI.js.

microshine commented 1 year ago

@gnarea @peculiar/x509 is based on this schema. PKIjs includes PKI logic and ASN.1 schemas. The simplest way to parse the Certificate using PKIjs is pkijs.Certificate.fromBER(raw).

gnarea commented 1 year ago

Thanks for looking into this @microshine!

Do you mean that pkijs.Certificate.fromBER(raw) can take an asn1-schema/x509 Certificate too, not just a buffer? I tried that but maybe I'm doing something wrong because I got AsnError: Error during parsing of ASN.1 data. Data is not correct for 'Certificate':

import { Certificate as Asn1Certificate } from '@peculiar/asn1-x509';
import { AsnParser } from '@peculiar/asn1-schema';
import { Certificate as PkijsCertificate } from 'pkijs';

const CERT = `MIIEczCCAyegAwIBAgIIf98Hu6tQXdMwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQMEAgEFAKEcMBoG
CSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgMIGOMYGLMIGIBgNVBAMegYAAOABiADIANAA0ADEA
ZgBjADEANwA2AGQANQA3ADUANQAxADIAOABmADEAZAA5ADgAYQA5ADcAZgAxAGEANAAyAGYAMQBhADQA
NwBjADgANgBhAGQAZAAwADEAZgBhADUAOAAxADcAZQA3ADgAZgA4ADkAMQA2AGYAOQBiADYAOTAeFw0y
MzAxMTMxMDQyMzNaFw0yMzAxMTQxMDQyMzNaMIGOMYGLMIGIBgNVBAMegYAAOABiADIANAA0ADEAZgBj
ADEANwA2AGQANQA3ADUANQAxADIAOABmADEAZAA5ADgAYQA5ADcAZgAxAGEANAAyAGYAMQBhADQANwBj
ADgANgBhAGQAZAAwADEAZgBhADUAOAAxADcAZQA3ADgAZgA4ADkAMQA2AGYAOQBiADYAOTCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8cE5qBNshuhv429ngLM1YfSOj+n5MentUr0/4cRLWPh0Qj
98VI3oZs7cvOfSkCeQgsIOhOmNZnC0wqAwo7dv1L2lJqi3975SplDe6VrWJSr6cJYLQxwkNFT6nYwPQ/
OvbqaZUGg4K5Qv5PEPDKO+wh78YfaeDDjMEbLoz0s0sNK/8OvESxH+fbbjwP2/r+Mhiy/PwjW9+Y5//P
Qb5LtKFLdpRqmU06cs3hDZ0Z9T1Mkohr2grp4OEPshgpixpnSSxQS3Sty3P+I08TclVDGsbr54uwXMV6
yHauyZfgw7LFJU6tpumYvwPs4v/w+0B543SQK15MC2H2xAcre75W8ncCAwEAAaNrMGkwDwYDVR0TAQH/
BAUwAwIBADArBgNVHSMEJDAigCCLJEH8F21XVRKPHZipfxpC8aR8hq3QH6WBfnj4kW+baTApBgNVHQ4E
IgQgiyRB/BdtV1USjx2YqX8aQvGkfIat0B+lgX54+JFvm2kwQQYJKoZIhvcNAQEKMDSgDzANBglghkgB
ZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgA4IBAQAgIgvgz3NQw0HW0tT/
imkaQEXAPdOVMJxjojrXmDSUMlBXlHuqcrOHcQxkAsun/QGK6G7MBdLIa64D5+adEtcbudei3EZ47uYM
BjaaY4haMJzvH6MH0k+FEjDsx85kl726v6mlpHEl1l2jLo8zXaHFmldXjFD4fKv7yIf+YR2UHBtiGu16
Ebnmvx5lePwSKau841LLrxPDy1jJbBKBO+/6uDgL49Gx2X6AxlCkYTakRitsTPBjGP4lVLWaX4VC1C/G
rWTAht1wd2thbxnYsdXFArpKLyiBf02RPibEqKFmcnjXOpt1CeJldX0J7EX5sF/uz2l/NQTh11LUkcRB
l9Lu`;

test('it', () => {
  const serialisation = Buffer.from(CERT, 'base64');
  const asn1 = AsnParser.parse(serialisation, Asn1Certificate);
  const pkijs = PkijsCertificate.fromBER(asn1 as any);
  expect(Buffer.from(pkijs.toSchema().toBER())).toStrictEqual(serialisation);
});

Note that the reason why I want to init a PKI.js Certificate with an asn1-schema/x509 Certificate, instead of a Buffer/ArrayBuffer, is that the X.509 is contained in an ASN.1 SEQUENCE that's already been parsed with asn1-schema:

import { AsnProp } from '@peculiar/asn1-schema';
import { Certificate } from '@peculiar/asn1-x509';

import { DnssecChain } from '../dns/DnssecChain.js';

export class MemberIdBundleSchema {
  @AsnProp({ type: DnssecChain })
  public dnssecChain!: DnssecChain;

  @AsnProp({ type: Certificate })
  public organisationCertificate!: Certificate;

  @AsnProp({ type: Certificate })
  public memberCertificate!: Certificate;
}

So if I already have an instance of MemberIdBundleSchema, which was created from a well-formed DER buffer, I'd prefer to have the option to pass the MemberIdBundleSchema.organisationCertificate value (for example), to PKI.js' Certificate.

This isn't the end of the world because I can always re-serialise the asn1-schema/x509 Certificate to an ArrayBuffer and have PKI.js' Certificate parse it, but it'd be easier and better for performance if we could skip the the re-serialisation.

microshine commented 1 year ago

Note that the reason why I want to init a PKI.js Certificate with an asn1-schema/x509 Certificate, instead of a Buffer/ArrayBuffer, is that the X.509 is contained in an ASN.1 SEQUENCE that's already been parsed with asn1-schema

I see what you are trying to do. You don't want to parse the Certificate raw twice. But it's impossible. PKIjs use its own ASN.1 schemas. Supporting of @peculiar/asn1-schema requires a major update of PKIjs with architecture changing. As I told before, each PKIjs class implements 2 logics - ASN.1 schema and PKI functions. If we use @peculiar/asn1-schema in PKIjs then PKIjs ASN.1 implementation becomes odd.

What do you use in PKIjs we don't have in @peculiar/x509?

gnarea commented 1 year ago

If we use @peculiar/asn1-schema in PKIjs then PKIjs ASN.1 implementation becomes odd.

Agreed 💯 . I was just checking if you'd be open to that, but this isn't blocking me so it's not the end of the world.

What do you use in PKIjs we don't have in @peculiar/x509?

I basically need to verify certification paths. The root/trusted certificate will be present in the ASN.1 SEQUENCE I mentioned above, and I want to pass that certificate to the trustedCerts param in SignedData.verify().

microshine commented 1 year ago

@peculiar/x509 has a simple chain validation. But it's based on certificate signatures. It doesn't check revocations and policies. I'd like to support the chain validation with those features, but I need time to implement it.

So if you need just chain without those validations, you can use X509ChainBuilder

https://peculiarventures.github.io/x509/classes/X509ChainBuilder.html

const chain = new x509.X509ChainBuilder({
  certificates: [
    new x509.X509Certificate(raw1),
    new x509.X509Certificate(raw2),
    // ...
    new x509.X509Certificate(rawN),
  ],
});

const cert = x509.X509Certificate(raw);
const items = await chain.build(cert);
gnarea commented 1 year ago

Thanks @microshine!

Though in my case, the signer's certificate and any intermediate certificates are embedded in the SignedData value, so I guess I'd have to re-serialise those certificates so I can build x509.X509Certificate instances.

In which case I think it'd be easier/faster to only re-serialise the root certificate and pass it to SignedData.verify() (trustedCerts param).