@blamejs/pki 0.6.48 → 0.6.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +2 -2
- package/lib/cmc-build.js +320 -9
- package/lib/cmc-verify.js +20 -0
- package/lib/cmp-build.js +184 -11
- package/lib/cmp-session.js +27 -0
- package/lib/cmp-verify.js +102 -6
- package/lib/cms-decrypt.js +49 -4
- package/lib/constants.js +3 -0
- package/lib/guard-intrinsic.js +6 -0
- package/lib/schema-cmc.js +201 -3
- package/lib/schema-cmp.js +45 -0
- package/lib/schema-cms.js +3 -1
- package/lib/sign-scheme.js +19 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-cmc.js
CHANGED
|
@@ -42,6 +42,8 @@ var schema = require("./schema-engine");
|
|
|
42
42
|
var pkix = require("./schema-pkix");
|
|
43
43
|
var oid = require("./oid");
|
|
44
44
|
var cms = require("./schema-cms");
|
|
45
|
+
var csr = require("./schema-csr");
|
|
46
|
+
var crmf = require("./schema-crmf");
|
|
45
47
|
var guard = require("./guard-all");
|
|
46
48
|
var frameworkError = require("./framework-error");
|
|
47
49
|
|
|
@@ -55,6 +57,8 @@ function O(name) { return oid.byName(name); }
|
|
|
55
57
|
|
|
56
58
|
var OID_PKI_DATA = O("id-cct-PKIData");
|
|
57
59
|
var OID_PKI_RESPONSE = O("id-cct-PKIResponse");
|
|
60
|
+
var OID_ENCRYPTED_POP = O("id-cmc-encryptedPOP");
|
|
61
|
+
var OID_DECRYPTED_POP = O("id-cmc-decryptedPOP");
|
|
58
62
|
var OID_STATUS_INFO = O("id-cmc-statusInfo");
|
|
59
63
|
var OID_STATUS_INFO_V2 = O("id-cmc-statusInfoV2");
|
|
60
64
|
var OID_RESPONSE_BODY = O("id-cmc-responseBody");
|
|
@@ -79,6 +83,11 @@ var CMC_CARRIERS = Object.assign(Object.create(null), { signedData: 1, authData:
|
|
|
79
83
|
var CONTROL_PLACEMENT = Object.create(null);
|
|
80
84
|
CONTROL_PLACEMENT[OID_RESPONSE_BODY] = "pkiResponse";
|
|
81
85
|
CONTROL_PLACEMENT[OID_RA_IDENTITY_WITNESS] = "pkiData";
|
|
86
|
+
/** @internal The two halves of the challenge-response exchange travel in opposite directions: the
|
|
87
|
+
* authority sends the challenge in its response, and the client answers in its next request (RFC 5272
|
|
88
|
+
* sec. 6.7). A message carrying the other one is not the message that section describes. */
|
|
89
|
+
CONTROL_PLACEMENT[OID_ENCRYPTED_POP] = "pkiResponse";
|
|
90
|
+
CONTROL_PLACEMENT[OID_DECRYPTED_POP] = "pkiData";
|
|
82
91
|
|
|
83
92
|
|
|
84
93
|
function readBodyPartId(node, label) {
|
|
@@ -429,6 +438,181 @@ var PKI_RESPONSE = schema.seq([
|
|
|
429
438
|
},
|
|
430
439
|
});
|
|
431
440
|
|
|
441
|
+
/** @internal The challenge-response proof of possession a server sends when the key being certified
|
|
442
|
+
* cannot sign for itself (RFC 5272 sec. 6.7). The `cms` field is surfaced as the bytes it arrived as,
|
|
443
|
+
* because it is an EnvelopedData a caller decrypts with its own key material rather than something
|
|
444
|
+
* this parser opens. */
|
|
445
|
+
/** @internal The request a challenge quotes is one the client answers for, so it is held to the
|
|
446
|
+
* definition of the arm it uses rather than to its outer tag alone: a proof computed over a request
|
|
447
|
+
* that is not one could never be checked against anything. This is asked of a CHALLENGED request only.
|
|
448
|
+
* A request body carries what its sender sent, and judging it is the enrollment's business, not the
|
|
449
|
+
* decoder's. */
|
|
450
|
+
function _assertAnswerableRequest(request) {
|
|
451
|
+
if (request.arm !== "tcr" && request.arm !== "crm") return;
|
|
452
|
+
/** @internal A CertReqMessages of one, which is the shape the CRMF parser takes; built outside the
|
|
453
|
+
* catch below so only the parse it wraps can be what fails. */
|
|
454
|
+
var payload = request.arm === "tcr" ? request.certificationRequestBytes
|
|
455
|
+
: asn1.build.sequence([asn1.build.raw(request.certReqMsgBytes)]);
|
|
456
|
+
try {
|
|
457
|
+
if (request.arm === "tcr") { csr.parse(payload); } else { crmf.parse(payload); }
|
|
458
|
+
} catch (e) {
|
|
459
|
+
throw E("cmc/bad-pop-challenge",
|
|
460
|
+
"the request an EncryptedPOP quotes is not a " +
|
|
461
|
+
(request.arm === "tcr" ? "PKCS#10 CertificationRequest" : "CRMF CertReqMsg") +
|
|
462
|
+
", so a proof over it could not be checked: " + ((e && e.message) || "malformed") +
|
|
463
|
+
" (RFC 5272 sec. 6.7)", e);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function parseEncryptedPop(node) {
|
|
468
|
+
if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.constructed ||
|
|
469
|
+
node.children.length !== 5) {
|
|
470
|
+
throw E("cmc/bad-pop-challenge",
|
|
471
|
+
"an EncryptedPOP is SEQUENCE { request, cms, thePOPAlgID, witnessAlgID, witness } (RFC 5272 sec. 6.7)");
|
|
472
|
+
}
|
|
473
|
+
var envelope = _assertEnvelopedChallenge(node.children[1]);
|
|
474
|
+
var request;
|
|
475
|
+
try { request = parseTaggedRequest(node.children[0]); }
|
|
476
|
+
catch (e) {
|
|
477
|
+
throw E("cmc/bad-pop-challenge",
|
|
478
|
+
"an EncryptedPOP quotes the request it challenges as a TaggedRequest: " +
|
|
479
|
+
((e && e.message) || "malformed") + " (RFC 5272 sec. 6.7)", e);
|
|
480
|
+
}
|
|
481
|
+
_assertAnswerableRequest(request);
|
|
482
|
+
return {
|
|
483
|
+
request: request,
|
|
484
|
+
/** @internal The TaggedRequest exactly as it arrived. A proof of possession is a MAC over these
|
|
485
|
+
* bytes, and the challenge carries them so neither side has to re-encode the request to agree on
|
|
486
|
+
* what was covered (RFC 5272 sec. 6.7). */
|
|
487
|
+
requestBytes: node.children[0].bytes,
|
|
488
|
+
cms: envelope.bytes,
|
|
489
|
+
cmsRecipientCount: envelope.recipientCount,
|
|
490
|
+
thePOPAlgID: _popAlgorithm(node.children[2], "thePOPAlgID"),
|
|
491
|
+
witnessAlgID: _popAlgorithm(node.children[3], "witnessAlgID"),
|
|
492
|
+
witness: _popOctetString(node.children[4], "witness"),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** @internal A leaf of one of the POP controls. A malformed one is a malformed challenge, so it carries
|
|
497
|
+
* this control's code rather than the codec's. */
|
|
498
|
+
function _popOctetString(node, what) {
|
|
499
|
+
try { return asn1.read.octetString(node); }
|
|
500
|
+
catch (e) {
|
|
501
|
+
throw E("cmc/bad-pop-challenge", "a POP " + what + " is an OCTET STRING (RFC 5272 sec. 6.7)", e);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function parseDecryptedPop(node) {
|
|
506
|
+
if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.constructed ||
|
|
507
|
+
node.children.length !== 3) {
|
|
508
|
+
throw E("cmc/bad-pop-challenge",
|
|
509
|
+
"a DecryptedPOP is SEQUENCE { bodyPartID, thePOPAlgID, thePOP } (RFC 5272 sec. 6.7)");
|
|
510
|
+
}
|
|
511
|
+
var bodyPartID;
|
|
512
|
+
try { bodyPartID = readBodyPartIdentity(node.children[0]); }
|
|
513
|
+
catch (e) {
|
|
514
|
+
throw E("cmc/bad-pop-challenge",
|
|
515
|
+
"a DecryptedPOP names the certification request it proves with a BodyPartID: " +
|
|
516
|
+
((e && e.message) || "malformed") + " (RFC 5272 sec. 6.7)", e);
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
bodyPartID: bodyPartID,
|
|
520
|
+
thePOPAlgID: _popAlgorithm(node.children[1], "thePOPAlgID"),
|
|
521
|
+
thePOP: _popOctetString(node.children[2], "thePOP"),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** @internal The proof value travels in an EnvelopedData (RFC 5272 sec. 6.7), and the bytes are handed
|
|
526
|
+
* on unopened because opening them needs key material this parser does not hold. Unopened is not
|
|
527
|
+
* unchecked: a field that is not a ContentInfo carrying an EnvelopedData cannot be a challenge, and
|
|
528
|
+
* saying so here is what keeps a control that could never be answered from reading as one. */
|
|
529
|
+
function _assertEnvelopedChallenge(node) {
|
|
530
|
+
var kids = pkix.rootSequenceChildren(node, 2, 2);
|
|
531
|
+
if (!kids) {
|
|
532
|
+
throw E("cmc/bad-pop-challenge",
|
|
533
|
+
"an EncryptedPOP carries the proof value in a CMS ContentInfo, which is a SEQUENCE of a content " +
|
|
534
|
+
"type and its content (RFC 5272 sec. 6.7)");
|
|
535
|
+
}
|
|
536
|
+
var contentType;
|
|
537
|
+
try { contentType = asn1.read.oid(kids[0]); }
|
|
538
|
+
catch (e) {
|
|
539
|
+
throw E("cmc/bad-pop-challenge",
|
|
540
|
+
"an EncryptedPOP's ContentInfo names its content type with an OBJECT IDENTIFIER (RFC 5272 sec. 6.7)", e);
|
|
541
|
+
}
|
|
542
|
+
if (contentType !== O("envelopedData")) {
|
|
543
|
+
throw E("cmc/bad-pop-challenge",
|
|
544
|
+
"an EncryptedPOP carries the proof value in an EnvelopedData, got " +
|
|
545
|
+
(oid.name(contentType) || contentType) + " (RFC 5272 sec. 6.7)");
|
|
546
|
+
}
|
|
547
|
+
var content = kids[1];
|
|
548
|
+
if (content.tagClass !== "context" || content.tagNumber !== 0 || !content.constructed ||
|
|
549
|
+
content.children.length !== 1) {
|
|
550
|
+
throw E("cmc/bad-pop-challenge",
|
|
551
|
+
"an EncryptedPOP's ContentInfo carries its content in [0] EXPLICIT (RFC 5652 sec. 3)");
|
|
552
|
+
}
|
|
553
|
+
/** @internal Validated by the CMS EnvelopedData schema itself rather than by a shape check written
|
|
554
|
+
* here, so every field is held to one definition and the count reported below is the count of
|
|
555
|
+
* recipients that same definition accepted. Sec. 6.7 also allows a recipient identified by an
|
|
556
|
+
* issuerAndSerialNumber whose issuer is encoded as NULL, which is not the Name that schema requires;
|
|
557
|
+
* a challenge addressed only that way is refused here, and `pki.cms.decrypt` could not open it
|
|
558
|
+
* either, so it is refused where the reason can be named rather than after a failed answer. */
|
|
559
|
+
var enveloped;
|
|
560
|
+
try { enveloped = cms.walkEnvelopedData(content.children[0]); }
|
|
561
|
+
catch (e) {
|
|
562
|
+
throw E("cmc/bad-pop-challenge",
|
|
563
|
+
"an EncryptedPOP's ContentInfo does not carry an EnvelopedData a recipient could open: " +
|
|
564
|
+
((e && e.message) || "malformed") + " (RFC 5652 sec. 6.1)", e);
|
|
565
|
+
}
|
|
566
|
+
var recipients = (enveloped.recipientInfos || []).length;
|
|
567
|
+
if (recipients < 1) {
|
|
568
|
+
throw E("cmc/bad-pop-challenge",
|
|
569
|
+
"an EnvelopedData names at least one recipient in its recipientInfos SET (RFC 5652 sec. 6.1)");
|
|
570
|
+
}
|
|
571
|
+
return { bytes: node.bytes, recipientCount: recipients };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** @internal Both fields are AlgorithmIdentifiers, and both are read the same way: the OID names the
|
|
575
|
+
* algorithm and the parameters travel with it, so a caller answering a challenge copies what it was
|
|
576
|
+
* sent rather than reconstructing it. */
|
|
577
|
+
function _popAlgorithm(node, what) {
|
|
578
|
+
if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.constructed ||
|
|
579
|
+
node.children.length < 1 || node.children.length > 2) {
|
|
580
|
+
throw E("cmc/bad-pop-challenge", "a POP " + what + " is an AlgorithmIdentifier (RFC 5272 sec. 6.7)");
|
|
581
|
+
}
|
|
582
|
+
var algOid;
|
|
583
|
+
try { algOid = asn1.read.oid(node.children[0]); }
|
|
584
|
+
catch (e) {
|
|
585
|
+
throw E("cmc/bad-pop-challenge",
|
|
586
|
+
"a POP " + what + " names its algorithm with an OBJECT IDENTIFIER (RFC 5272 sec. 6.7)", e);
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
oid: algOid,
|
|
590
|
+
name: oid.name(algOid) || null,
|
|
591
|
+
parameters: node.children.length === 2 ? node.children[1].bytes : null,
|
|
592
|
+
bytes: node.bytes,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** @internal One AttributeValue, which is what a control carrying a single structure holds. */
|
|
597
|
+
function _onlyValue(c, what) {
|
|
598
|
+
if (c.values.length !== 1) {
|
|
599
|
+
throw E("cmc/bad-pop-challenge",
|
|
600
|
+
"a " + what + " control carries exactly one AttributeValue, got " + c.values.length);
|
|
601
|
+
}
|
|
602
|
+
return asn1.decode(c.values[0]);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function _decodePopChallenges(controls) {
|
|
606
|
+
for (var i = 0; i < controls.length; i++) {
|
|
607
|
+
var c = controls[i];
|
|
608
|
+
if (c.attrType === OID_ENCRYPTED_POP) {
|
|
609
|
+
c.encryptedPOP = parseEncryptedPop(_onlyValue(c, "an Encrypted POP"));
|
|
610
|
+
} else if (c.attrType === OID_DECRYPTED_POP) {
|
|
611
|
+
c.decryptedPOP = parseDecryptedPop(_onlyValue(c, "a Decrypted POP"));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
432
616
|
function _collectStatuses(controls) {
|
|
433
617
|
var out = [];
|
|
434
618
|
for (var i = 0; i < controls.length; i++) {
|
|
@@ -445,6 +629,7 @@ function _collectStatuses(controls) {
|
|
|
445
629
|
|
|
446
630
|
function _finishBody(body, where) {
|
|
447
631
|
for (var i = 0; i < body.controls.length; i++) assertControlPlacement(body.controls[i], where);
|
|
632
|
+
_decodePopChallenges(body.controls);
|
|
448
633
|
body.statuses = _collectStatuses(body.controls);
|
|
449
634
|
body.unhandled = body.controls.filter(function (c) {
|
|
450
635
|
return c.attrType !== OID_STATUS_INFO && c.attrType !== OID_STATUS_INFO_V2 && c.attrName === null;
|
|
@@ -541,9 +726,20 @@ function parsePkiResponse(input) {
|
|
|
541
726
|
* pki.schema.cmc.parse(der).kind; // "pkiData"
|
|
542
727
|
*/
|
|
543
728
|
function parse(input) {
|
|
544
|
-
var
|
|
545
|
-
!guard.bytes.isByteSource(input) && input.encapContentInfo != null
|
|
546
|
-
|
|
729
|
+
var alreadyParsed = input && typeof input === "object" && !ArrayBuffer.isView(input) &&
|
|
730
|
+
!guard.bytes.isByteSource(input) && input.encapContentInfo != null;
|
|
731
|
+
var parsedCms;
|
|
732
|
+
if (alreadyParsed) { parsedCms = input; }
|
|
733
|
+
else {
|
|
734
|
+
/** @internal Bytes that are not a CMS message at all are a CMC message this cannot read, and every
|
|
735
|
+
* other format parser answers for its own input in its own domain. */
|
|
736
|
+
try { parsedCms = cms.parse(input); }
|
|
737
|
+
catch (e) {
|
|
738
|
+
throw E("cmc/bad-der",
|
|
739
|
+
"a CMC message is carried in a CMS SignedData or AuthenticatedData, and these bytes are not " +
|
|
740
|
+
"one: " + ((e && e.message) || "malformed") + " (RFC 5272 sec. 3.2 / 4.2)", e);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
547
743
|
if (CMC_CARRIERS[parsedCms.contentTypeName] !== 1) {
|
|
548
744
|
throw E("cmc/not-cmc",
|
|
549
745
|
"a Full PKI Request / Response is carried in a CMS SignedData or AuthenticatedData, got " +
|
|
@@ -568,6 +764,8 @@ function parse(input) {
|
|
|
568
764
|
}
|
|
569
765
|
|
|
570
766
|
module.exports = {
|
|
767
|
+
parseEncryptedPop: parseEncryptedPop,
|
|
768
|
+
parseTaggedRequest: parseTaggedRequest,
|
|
571
769
|
parse: parse,
|
|
572
770
|
parsePkiData: parsePkiData,
|
|
573
771
|
parsePkiResponse: parsePkiResponse,
|
package/lib/schema-cmp.js
CHANGED
|
@@ -43,6 +43,7 @@ var cms = require("./schema-cms.js");
|
|
|
43
43
|
var crmf = require("./schema-crmf.js");
|
|
44
44
|
var frameworkError = require("./framework-error.js");
|
|
45
45
|
var guard = require("./guard-all.js");
|
|
46
|
+
var constants = require("./constants");
|
|
46
47
|
|
|
47
48
|
var CmpError = frameworkError.CmpError;
|
|
48
49
|
var PemError = frameworkError.PemError;
|
|
@@ -101,6 +102,40 @@ var GENERAL_NAME = pkix.generalName(NS, { decodeValue: true });
|
|
|
101
102
|
var ALG_ID = pkix.algorithmIdentifier(NS);
|
|
102
103
|
var EXTENSIONS = pkix.extensions(NS);
|
|
103
104
|
|
|
105
|
+
/** @internal The parameters of a KEM-based MAC protectionAlg (RFC 9810 sec. 5.1.3.4). `len` is the
|
|
106
|
+
* size of the key the KDF produces, and it is the size the MAC algorithm needs, so the two travel
|
|
107
|
+
* together and a verifier derives exactly what the protector derived. */
|
|
108
|
+
var KEM_BM_PARAMETER = schema.seq([
|
|
109
|
+
schema.field("kdf", ALG_ID),
|
|
110
|
+
/** @internal The CMP module is DEFINITIONS EXPLICIT TAGS, so `[0]` wraps the OCTET STRING rather
|
|
111
|
+
* than replacing its tag. */
|
|
112
|
+
schema.optional("kemContext", schema.octetString(), { tag: 0, explicit: true }),
|
|
113
|
+
schema.field("len", schema.integerLeaf()),
|
|
114
|
+
schema.field("mac", ALG_ID),
|
|
115
|
+
], {
|
|
116
|
+
assert: "sequence", arity: { min: 3, max: 4 }, code: "cmp/bad-kem-data", what: "KemBMParameter",
|
|
117
|
+
build: function (m, ctx) {
|
|
118
|
+
var n = guard.range.int(m.fields.len.value, 1n, BigInt(constants.LIMITS.CMP_KEM_MAX_KEY_BYTES),
|
|
119
|
+
ctx.E, "cmp/bad-kem-data", "a KemBMParameter len");
|
|
120
|
+
return {
|
|
121
|
+
kdf: m.fields.kdf.value.result,
|
|
122
|
+
kemContext: m.fields.kemContext.present ? m.fields.kemContext.value : null,
|
|
123
|
+
len: n,
|
|
124
|
+
mac: m.fields.mac.value.result,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/** @internal The KEM ciphertext a peer sends so the holder of the KEM private key can derive the same
|
|
130
|
+
* secret (RFC 9810 sec. 5.3.19.18). */
|
|
131
|
+
var KEM_CIPHERTEXT_INFO = schema.seq([
|
|
132
|
+
schema.field("kem", ALG_ID),
|
|
133
|
+
schema.field("ct", schema.octetString()),
|
|
134
|
+
], {
|
|
135
|
+
assert: "sequence", arity: { exact: 2 }, code: "cmp/bad-kem-ciphertext", what: "KemCiphertextInfo",
|
|
136
|
+
build: function (m) { return { kem: m.fields.kem.value.result, ct: m.fields.ct.value }; },
|
|
137
|
+
});
|
|
138
|
+
|
|
104
139
|
|
|
105
140
|
var INFO_TYPE_AND_VALUE = schema.seq([
|
|
106
141
|
schema.field("infoType", schema.oidLeaf()),
|
|
@@ -835,6 +870,14 @@ function matches(root) {
|
|
|
835
870
|
return schema.isContextInRange(body, 0, 26) && !!body.children;
|
|
836
871
|
}
|
|
837
872
|
|
|
873
|
+
/** @internal One embedded KEM structure, read under this module's namespace so every field of it
|
|
874
|
+
* reports in the CMP domain rather than the codec's. */
|
|
875
|
+
function _readKem(sch, der, code, what) {
|
|
876
|
+
return schema.embeddedDer(sch, der, NS, { code: code, what: what }).result;
|
|
877
|
+
}
|
|
878
|
+
function readKemBMParameter(p) { return _readKem(KEM_BM_PARAMETER, p, "cmp/bad-kem-data", "KemBMParameter"); }
|
|
879
|
+
function readKemCiphertextInfo(d) { return _readKem(KEM_CIPHERTEXT_INFO, d, "cmp/bad-kem-ciphertext", "KemCiphertextInfo"); }
|
|
880
|
+
|
|
838
881
|
module.exports = {
|
|
839
882
|
parse: parse,
|
|
840
883
|
pemDecode: pemDecode,
|
|
@@ -847,4 +890,6 @@ module.exports = {
|
|
|
847
890
|
readCrls: readCrls,
|
|
848
891
|
readAlgorithmIdentifier: readAlgorithmIdentifier,
|
|
849
892
|
readName: readName,
|
|
893
|
+
readKemBMParameter: readKemBMParameter,
|
|
894
|
+
readKemCiphertextInfo: readKemCiphertextInfo,
|
|
850
895
|
};
|
package/lib/schema-cms.js
CHANGED
|
@@ -555,7 +555,9 @@ var KEY_AGREE_RECIPIENT_INFO = schema.seq([
|
|
|
555
555
|
schema.field("originator", schema.explicit(0, ORIGINATOR_IDENTIFIER_OR_KEY, { code: "cms/bad-kari" })),
|
|
556
556
|
schema.optional("ukm", schema.octetString(), { tag: 1, explicit: true, emptyCode: "cms/bad-kari" }),
|
|
557
557
|
schema.field("keyEncryptionAlgorithm", ALGORITHM_IDENTIFIER),
|
|
558
|
-
|
|
558
|
+
/** @internal At least one encrypted key: a key-agreement recipient info that names none names no
|
|
559
|
+
* recipient, so nothing could open the content it is attached to. */
|
|
560
|
+
schema.field("recipientEncryptedKeys", schema.seqOf(RECIPIENT_ENCRYPTED_KEY, { min: 1, code: "cms/bad-recipient-encrypted-keys", what: "recipientEncryptedKeys" })),
|
|
559
561
|
], {
|
|
560
562
|
assert: "constructed", code: "cms/bad-kari", what: "KeyAgreeRecipientInfo",
|
|
561
563
|
build: function (m) {
|
package/lib/sign-scheme.js
CHANGED
|
@@ -164,10 +164,23 @@ function _assertKeyMatchesScheme(key, imp, E) {
|
|
|
164
164
|
if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
|
|
165
165
|
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
166
166
|
}
|
|
167
|
-
|
|
167
|
+
/** @internal `owned`, when given, collects the copies this makes of the caller's key so the caller can
|
|
168
|
+
* wipe them: a Buffer is viewed and stays the caller's, while a Uint8Array or a PEM string produces a
|
|
169
|
+
* new buffer holding the private key. The branch is taken once here, so no caller has to re-derive
|
|
170
|
+
* which shape it passed. */
|
|
171
|
+
function _normPkcs8(k, label, E, owned) {
|
|
168
172
|
if (_isBuffer(k)) return guard.bytes.view(k, E, "bad-input", label);
|
|
169
|
-
if (_isU8(k))
|
|
170
|
-
|
|
173
|
+
if (_isU8(k)) {
|
|
174
|
+
var snap = guard.bytes.snapshot(k, E, "bad-input", label);
|
|
175
|
+
if (owned) guard.list.append(owned, snap);
|
|
176
|
+
return snap;
|
|
177
|
+
}
|
|
178
|
+
if (typeof k === "string") {
|
|
179
|
+
var der;
|
|
180
|
+
try { der = pkcs8.pemDecode(k); } catch (e) { throw E("bad-input", label + " PEM could not be decoded", e); }
|
|
181
|
+
if (owned) guard.list.append(owned, der);
|
|
182
|
+
return der;
|
|
183
|
+
}
|
|
171
184
|
throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
172
185
|
}
|
|
173
186
|
function _normCompositeKeys(key, comp, E) {
|
|
@@ -220,6 +233,9 @@ function signOverTbs(scheme, key, signedBytes, E) {
|
|
|
220
233
|
|
|
221
234
|
module.exports = {
|
|
222
235
|
resolveSignScheme: resolveSignScheme,
|
|
236
|
+
/** @internal A PKCS#8 private key from the shapes every signer option accepts, so a verb that takes
|
|
237
|
+
* a private key for something other than signing reads it the same way. */
|
|
238
|
+
normalizePkcs8: _normPkcs8,
|
|
223
239
|
pssSpkiPinnedHash: pssSpkiPinnedHash,
|
|
224
240
|
signOverTbs: signOverTbs,
|
|
225
241
|
MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:e7a3f132-aaea-4bc5-ae0f-387a72dc5e4f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-07T05:02:17.771Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.6.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.6.50",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.6.
|
|
25
|
+
"version": "0.6.50",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.6.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.6.50",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.6.
|
|
57
|
+
"ref": "@blamejs/pki@0.6.50",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|