asn1-schema icon indicating copy to clipboard operation
asn1-schema copied to clipboard

AsnConvert an array to a Set

Open dhensby opened this issue 3 years ago • 6 comments

I need to convert an array of Attributes to an encoded Set, however passing a raw array to AsnConvert results in Error: Cannot get schema for 'Array' target (unsurprisingly).

example code:

const { AsnConvert } = require('@peculiar/asn1-schema');
const { Attribtue } = require('@peculiar/asn1-cms');
const { id_pkcs9_at_messageDigest } = require('@peculiar/asn1-pkcs9');

const attributes = [new Attribute({
  attrType: id_pkcs9_at_messageDigest,
  attrValues: [Buffer.from(...)],
});

const encoded = AsnConvert.serialize(attributes); // errors

dhensby avatar May 11 '21 19:05 dhensby

This is specifically a problem for what I'm trying to do which is sign the signedAttrs of SignerInfo. To sign them the attribute needs to be encoded as a set, but the SignedAttributes is just a type (export declare type SignedAttributes = Attribute[]) and not an actual constructor. If this were defined as a class, I think it would solve the problem, allowing the attributes to be encoded.

I would suspect this could be a problem in any area where there's no canonical definition of a type and instead it's just declared as a AsnProp on a class but can't be encoded in a standalone way. Maybe it would be cool if you could do something like AsnConvert.serialize(attributes, SigerInfo.signedAttrs).

Another example would be encoding OIDs (AsnConvert.serialize(oid) - results in Error: Cannot get schema for 'String' target)

dhensby avatar May 12 '21 10:05 dhensby

const signedData = new SignedData();
signedData.signerInfos.push(new SignerInfo({
  signedAttrs: [
    new Attribute({
      attrType: "1.2.3.4.5.6",
      attrValues: [
        new Uint8Array([2, 1, 3]).buffer,
      ]
    })
  ]
}));

const raw = AsnConvert.serialize(signedData);
console.log(Convert.ToHex(raw));

Output

30300201003100300206003125302302010030043000020030020600a00e300c06052a030405063103020103300206000400

ASN.1

SEQUENCE (4 elem)
  INTEGER 0
  SET (0 elem)
  SEQUENCE (1 elem)
    OBJECT IDENTIFIER
  SET (1 elem)
    SEQUENCE (6 elem)
      INTEGER 0
      SEQUENCE (2 elem)
        SEQUENCE (0 elem)
        INTEGER 0
      SEQUENCE (1 elem)
        OBJECT IDENTIFIER
      [0] (1 elem)
        SEQUENCE (2 elem)
          OBJECT IDENTIFIER 1.2.3.4.5.6
          SET (1 elem)
            INTEGER 3
      SEQUENCE (1 elem)
        OBJECT IDENTIFIER
      OCTET STRING (0 byte)

microshine avatar May 12 '21 11:05 microshine

Method serialize doesn't support simple times (eg number, string, boolean, array, etc). I think it would be better to add fromJSON method to AsnConvert class for such cases.

AsnConvert.fromJSON(1); // INTEGER 1
AsnConvert.fromJSON("Hello"); // PrintableString Hello
AsnConvert.fromJSON([ 1, 2, 3]); // SET INTEGER 1 INTEGER 2 INTEGER 3

@dhensby What do you think about it?

microshine avatar May 12 '21 11:05 microshine

@microshine - from your first reply, that encodes the entire SignedData object, but for the signed data we need to encode the signedAttrs prop (as a SET) and sign that. Which can't be done as it stands.

In terms of a fromJSON prop, possibly, but what if we need to force the time, for example: AsnConver.fromJSON([1, 2, 3]) could be a SET or a SEQUENCE (right?) and likewise, some could be implicit, have context values, etc.

I feel like the "right" response would be that the props weren't assigned their encoding rules only by the annotation, but also by having content types that can be encoded standalone.

At the moment SignedAttributes is just a type defined as an array of Attributes. But if this was a class which was annotated as a SET with itemTypes, this could be encoded standalone.

Perhaps even having a standalone "basic" type of SET would work too (which, in fact I've done as a little polyfill for now):

@AsnType({ type: AsnTypeTypes.Set, itemType: AsnPropTypes.Any })
export class Set<T> extends AsnArray<T> {
}

dhensby avatar May 13 '21 15:05 dhensby

Here's a workaround to get the data that should be signed (the [3] is because the third attribute in my CMS is the message digest, adjust accordingly):

const toBeSigned = Buffer.from(
	(asn1js.fromBER(asn1Schema.AsnSerializer.serialize(signerInfo)).result.valueBlock as any).value[3]
		.valueBeforeDecode,
	'hex',
);
toBeSigned[0] = 0x31; // See https://datatracker.ietf.org/doc/html/rfc5652#section-5.4

andsens avatar Feb 12 '24 14:02 andsens

I'm just doing this at the moment to get around it:

const {
    AsnType,
    AsnArray,
    AsnTypeTypes,
    AsnPropTypes,
} = require('@peculiar/asn1-schema');

class Set extends AsnArray {}

// this is a little hack to allow us to define an Asn1 type `Set` which acts as an array
AsnType({ type: AsnTypeTypes.Set, itemType: AsnPropTypes.Any })(Set);

module.exports = { Set };

Then I can encode the Set (truncated example):

            // signed attributes must be an ordered set
            const signedAttrs = new Set(authenticatedAttributes.map(({ type, value }) => {
                return AsnConvert.serialize(new Attribute({
                    attrType: type,
                    attrValues: [AsnConvert.serialize(value)],
                }));
            }).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))));
            // encode the Set for signing
            const encodedAttrs = AsnConvert.serialize(signedAttrs);
            // perform your signing somehow
            const signature = await signer(Buffer.from(encodedAttrs));
            // construct the signer info for use in SignedData
            return new SignerInfo({
                version: CMSVersion.v1,
                sid: new SignerIdentifier({
                    issuerAndSerialNumber: new IssuerAndSerialNumber({
                        issuer: certificate.tbsCertificate.issuer,
                        serialNumber: certificate.tbsCertificate.serialNumber,
                    }),
                }),
                digestAlgorithm: new DigestAlgorithmIdentifier({
                    algorithm: digestAlgOid,
                    parameters: null,
                }),
                // it would be nice to re-use the `signedAttrs` from above, but I'm not sure that's possible
                signedAttrs: authenticatedAttributes.map(({ type, value }) => {
                    return new Attribute({
                        attrType: type,
                        attrValues: [AsnConvert.serialize(value)],
                    });
                }).sort((a, b) => Buffer.compare(Buffer.from(AsnConvert.serialize(a)), Buffer.from(AsnConvert.serialize(b)))),
                signatureAlgorithm: new SignatureAlgorithmIdentifier({
                    algorithm: id_rsaEncryption,
                    parameters: null,
                }),
                signature: new OctetString(signature),
            });

dhensby avatar Feb 12 '24 15:02 dhensby