asn1-schema
asn1-schema copied to clipboard
AsnConvert an array to a Set
I need to convert an array of Attribute
s 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
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
)
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)
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 - 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> {
}
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
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),
});