@blamejs/pki 0.6.51 → 0.6.52
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 +33 -0
- package/README.md +1 -1
- package/index.js +8 -3
- package/lib/cmp-build.js +23 -2
- package/lib/cmp-session.js +345 -84
- package/lib/cmp-verify.js +314 -0
- package/lib/guard-name.js +59 -1
- package/lib/guard-parsed.js +17 -1
- package/lib/key.js +159 -0
- package/lib/oid.js +2 -0
- package/lib/path-validate.js +8 -0
- package/lib/schema-cmp.js +1 -1
- package/lib/schema-pkix.js +43 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/cmp-verify.js
CHANGED
|
@@ -32,6 +32,10 @@ var asn1 = require("./asn1-der");
|
|
|
32
32
|
var oid = require("./oid");
|
|
33
33
|
var cmpBuild = require("./cmp-build");
|
|
34
34
|
var cmp = require("./schema-cmp");
|
|
35
|
+
var cms = require("./schema-cms");
|
|
36
|
+
var cmsVerify = require("./cms-verify");
|
|
37
|
+
var cmsDecrypt = require("./cms-decrypt");
|
|
38
|
+
var pkcs8 = require("./schema-pkcs8");
|
|
35
39
|
var pkix = require("./schema-pkix");
|
|
36
40
|
var pbes2 = require("./pbes2");
|
|
37
41
|
var x509 = require("./schema-x509");
|
|
@@ -840,6 +844,315 @@ async function _verify(message, opts) {
|
|
|
840
844
|
return _verifySignature(m, protectedPart, protectionAlg, protection, opts);
|
|
841
845
|
}
|
|
842
846
|
|
|
847
|
+
var KNOWN_OPEN_KEY_PACKAGE_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
848
|
+
key: 1, cert: 1, password: 1, trustAnchors: 1, intermediates: 1, time: 1,
|
|
849
|
+
authorizedBySharedSecret: 1,
|
|
850
|
+
});
|
|
851
|
+
var OID_A_KEY_PACKAGE = oid.byName("aKeyPackage");
|
|
852
|
+
var OID_SIGNED_DATA = oid.byName("signedData");
|
|
853
|
+
var OID_ENVELOPED_DATA = oid.byName("envelopedData");
|
|
854
|
+
var OID_CM_KGA = oid.byName("cmKGA");
|
|
855
|
+
|
|
856
|
+
function _keyPackageErr(reason, cause) { return _err("cmp/bad-key-package", reason, cause); }
|
|
857
|
+
|
|
858
|
+
/** @internal The three shapes sec. 4.1.6 profiles out of an EnvelopedData, read from the parsed
|
|
859
|
+
* container before anything is decrypted: the structure is an EnvelopedData, exactly one party can
|
|
860
|
+
* open it, and what it holds is a SignedData. */
|
|
861
|
+
function _assertKeyPackageContainer(parsed) {
|
|
862
|
+
if (parsed.contentTypeName !== "envelopedData") {
|
|
863
|
+
throw _keyPackageErr("a delivered key package is carried in a CMS EnvelopedData, and this container is a " +
|
|
864
|
+
parsed.contentTypeName + " (RFC 9483 sec. 4.1.6)");
|
|
865
|
+
}
|
|
866
|
+
var ris = parsed.recipientInfos;
|
|
867
|
+
if (!_isArray(ris) || ris.length !== 1) {
|
|
868
|
+
throw _keyPackageErr("a delivered key package names exactly one RecipientInfo, and this container names " +
|
|
869
|
+
guard.text.showValue(ris && ris.length) +
|
|
870
|
+
"; a second recipient is a second party able to open a key generated for this entity (RFC 9483 sec. 4.1.6)");
|
|
871
|
+
}
|
|
872
|
+
/** @internal A `KeyAgreeRecipientInfo` wraps the content-encryption key once per entry in its
|
|
873
|
+
* `recipientEncryptedKeys` (RFC 5652 sec. 6.2.2), each for a different recipient under one
|
|
874
|
+
* originator, so counting RecipientInfos alone would admit a second party under one of them. */
|
|
875
|
+
var reks = ris[0].recipientEncryptedKeys;
|
|
876
|
+
if (_isArray(reks) && reks.length !== 1) {
|
|
877
|
+
throw _keyPackageErr("the key agreement RecipientInfo of a delivered key package wraps the content-encryption key once, and this one wraps it " +
|
|
878
|
+
reks.length + " times; each is a party able to open a key generated for this entity (RFC 9483 sec. 4.1.6, RFC 5652 sec. 6.2.2)");
|
|
879
|
+
}
|
|
880
|
+
var eci = parsed.encryptedContentInfo;
|
|
881
|
+
if (!eci || eci.contentType !== OID_SIGNED_DATA) {
|
|
882
|
+
throw _keyPackageErr("the encrypted content of a delivered key package is declared id-signedData, and this container declares " +
|
|
883
|
+
guard.text.showValue(eci && eci.contentType) + " (RFC 9483 sec. 4.1.6)");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** @internal RFC 5958 sec. 2: AsymmetricKeyPackage ::= SEQUENCE SIZE (1..MAX) OF OneAsymmetricKey.
|
|
888
|
+
* Each element is surfaced as its own DER buffer, which is what a PKCS#8 `PrivateKeyInfo` is
|
|
889
|
+
* (`PrivateKeyInfo ::= OneAsymmetricKey`), so a caller imports it without re-encoding. Copied off the
|
|
890
|
+
* decrypted content rather than sliced from it, so holding one key does not hold the package. */
|
|
891
|
+
var ASYMMETRIC_KEY_PACKAGE = schema.seqOf(
|
|
892
|
+
schema.decode(function (n, ctx) {
|
|
893
|
+
if (!(n.tagClass === "universal" && n.tagNumber === asn1.TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
|
|
894
|
+
throw ctx.E("cmp/bad-key-package", "an AsymmetricKeyPackage element must be a OneAsymmetricKey SEQUENCE (RFC 5958 sec. 2)");
|
|
895
|
+
}
|
|
896
|
+
/** @internal OneAsymmetricKey and a PKCS#8 PrivateKeyInfo are the same structure (RFC 5958
|
|
897
|
+
* sec. 2), so the shipped strict parser decides whether an element is one; a SEQUENCE that merely
|
|
898
|
+
* looks like a container is not a delivered key. */
|
|
899
|
+
try { pkcs8.parse(n.bytes); }
|
|
900
|
+
catch (e) { throw ctx.E("cmp/bad-key-package", "an AsymmetricKeyPackage element is not a OneAsymmetricKey (RFC 5958 sec. 2)", e); }
|
|
901
|
+
return _bufferFrom(n.bytes);
|
|
902
|
+
}),
|
|
903
|
+
{ assert: "sequence", min: 1, code: "cmp/bad-key-package", what: "AsymmetricKeyPackage",
|
|
904
|
+
build: function (m) { return _map(m.items, function (it) { return it.value; }); } });
|
|
905
|
+
|
|
906
|
+
function _readKeyPackage(der) {
|
|
907
|
+
var node;
|
|
908
|
+
try { node = asn1.decode(der); }
|
|
909
|
+
catch (e) { throw _keyPackageErr("the signed content is not DER", e); }
|
|
910
|
+
return schema.walk(ASYMMETRIC_KEY_PACKAGE, node, NS).result;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function _fixOpenKeyPackageOptions(opts, made) {
|
|
914
|
+
opts = guard.identifier.optionsObject(opts, _err, "cmp/bad-input", "pki.cmp.openKeyPackage options");
|
|
915
|
+
guard.identifier.assertKnownKeys(opts, KNOWN_OPEN_KEY_PACKAGE_OPTS, _err, "cmp/bad-input", "unknown opts field ");
|
|
916
|
+
var f = {
|
|
917
|
+
key: opts.key,
|
|
918
|
+
cert: _fixByteish(opts.cert, "cert"),
|
|
919
|
+
password: _fixSecret(opts.password, made, "password"),
|
|
920
|
+
trustAnchors: _fixCertList(opts.trustAnchors, "trustAnchors"),
|
|
921
|
+
intermediates: _fixPoolCerts(opts.intermediates, "intermediates"),
|
|
922
|
+
time: _fixTime(opts.time),
|
|
923
|
+
exempt: opts.authorizedBySharedSecret === true,
|
|
924
|
+
};
|
|
925
|
+
if (opts.authorizedBySharedSecret != null && typeof opts.authorizedBySharedSecret !== "boolean") {
|
|
926
|
+
throw _err("cmp/bad-input", "opts.authorizedBySharedSecret is a boolean stating that this entity protected its request with the shared secret");
|
|
927
|
+
}
|
|
928
|
+
if (f.key == null && f.password == null) {
|
|
929
|
+
throw _err("cmp/bad-input", "opening a delivered key package needs the key material the content-encryption key was protected to: opts.key (with opts.cert) for the key transport and key agreement techniques, or opts.password for the password technique (RFC 9483 sec. 4.1.6.1 to 4.1.6.3)");
|
|
930
|
+
}
|
|
931
|
+
if (f.key != null && f.password != null) {
|
|
932
|
+
throw _err("cmp/bad-input", "a delivered key package is opened by one key management technique: pass opts.key or opts.password, not both");
|
|
933
|
+
}
|
|
934
|
+
/** @internal sec. 4.1.6 offers the shared-secret exemption only to an entity that protected its
|
|
935
|
+
* request with a MAC, which is the entity opening this container with that secret. Stating it
|
|
936
|
+
* alongside a private key claims the exemption for a signature-protected exchange, where the
|
|
937
|
+
* section's requirement to validate the signer stands. */
|
|
938
|
+
if (f.exempt && f.password == null) {
|
|
939
|
+
throw _err("cmp/bad-input", "opts.authorizedBySharedSecret authorizes the key generation authority by the secret that protected the request, so it applies only to a container opened with opts.password (RFC 9483 sec. 4.1.6)");
|
|
940
|
+
}
|
|
941
|
+
if (f.exempt && f.trustAnchors != null) {
|
|
942
|
+
throw _err("cmp/bad-input", "opts.authorizedBySharedSecret and opts.trustAnchors state two different rules for authorizing the key generation authority; supply one");
|
|
943
|
+
}
|
|
944
|
+
if (!f.exempt && f.trustAnchors == null) {
|
|
945
|
+
throw _err("cmp/bad-input", "authorizing the key generation authority needs opts.trustAnchors, or opts.authorizedBySharedSecret when this entity protected its request with the shared secret (RFC 9483 sec. 4.1.6)");
|
|
946
|
+
}
|
|
947
|
+
return f;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function _openKeyPackage(container, opts) {
|
|
951
|
+
var bytes = _asContentInfo(
|
|
952
|
+
guard.bytes.snapshotSource(_coerceKeyPackageBytes(container), CmpError, "cmp/bad-input", "the delivered key package container"),
|
|
953
|
+
OID_ENVELOPED_DATA, "the delivered key package container");
|
|
954
|
+
var parsed;
|
|
955
|
+
try { parsed = cms.parse(bytes); }
|
|
956
|
+
catch (e) { throw _keyPackageErr("the delivered key package container is not a CMS ContentInfo", e); }
|
|
957
|
+
_assertKeyPackageContainer(parsed);
|
|
958
|
+
|
|
959
|
+
/** @internal The profile fixes the recipient count at one, so the arm to open is named instead of
|
|
960
|
+
* matched: a caller holding only the private key need not supply the certificate as well. */
|
|
961
|
+
/** @internal Every way the container fails to open reports the one code, so the verdict carries no
|
|
962
|
+
* more than "it did not open": `pki.cms.decrypt` already collapses each secret-dependent failure to
|
|
963
|
+
* a single uniform result, and splitting that back out here would rebuild the oracle it removes. */
|
|
964
|
+
var opened;
|
|
965
|
+
try {
|
|
966
|
+
opened = await cmsDecrypt.decrypt(bytes, opts.password != null ? { password: opts.password } : { key: opts.key, cert: opts.cert },
|
|
967
|
+
{ recipientIndex: 0 });
|
|
968
|
+
} catch (e) {
|
|
969
|
+
throw _keyPackageErr("the delivered key package could not be opened under the supplied key material", e);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
var verdict;
|
|
973
|
+
var signedData = _asContentInfo(opened.content, OID_SIGNED_DATA, "the encrypted content");
|
|
974
|
+
try { verdict = await cmsVerify.verify(signedData, _signedDataVerifyOpts(opts)); }
|
|
975
|
+
catch (e) { throw _keyPackageErr("the SignedData wrapping the key package could not be verified", e); }
|
|
976
|
+
if (!verdict.valid) {
|
|
977
|
+
throw _keyPackageErr("the signature over the delivered key package does not verify; the SignedData reports [" +
|
|
978
|
+
intrinsic.join(_map(verdict.signers, function (s) { return s.code || "a signature mismatch"; }), ", ") + "]");
|
|
979
|
+
}
|
|
980
|
+
if (verdict.eContentType !== OID_A_KEY_PACKAGE) {
|
|
981
|
+
throw _keyPackageErr("the signed content type is " + verdict.eContentType + ", not id-ct-KP-aKeyPackage; the signature must bind what it covers to a key package (RFC 9483 sec. 4.1.6, RFC 5958 sec. 2)");
|
|
982
|
+
}
|
|
983
|
+
var signerCert = verdict.signers[0] && verdict.signers[0].cert;
|
|
984
|
+
if (!signerCert) throw _keyPackageErr("the SignedData names no signer certificate to authorize as the key generation authority");
|
|
985
|
+
_authorizeKga(verdict, signerCert, opts);
|
|
986
|
+
if (verdict.eContent == null) throw _keyPackageErr("the SignedData carries no encapsulated content, so no key package was delivered");
|
|
987
|
+
return { keys: _readKeyPackage(verdict.eContent), kga: _bufferFrom(signerCert), trusted: opts.exempt ? false : true };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** @internal sec. 4.1.6: the entity "MUST use a certificate containing the additional extended key
|
|
991
|
+
* usage extension id-kp-cmKGA in order to be accepted by the EE as a legitimate key generation
|
|
992
|
+
* authority", and the EE "MUST validate the signer certificate". `pki.path.validate`'s `requiredEku`
|
|
993
|
+
* answers the RFC 5280 sec. 4.2.1.12 question (does this certificate PERMIT the purpose), which an
|
|
994
|
+
* absent extension satisfies, so the assertion is checked here as well as the chain. */
|
|
995
|
+
function _authorizeKga(verdict, signerCert, opts) {
|
|
996
|
+
if (opts.exempt) return;
|
|
997
|
+
if (!verdict.trusted) {
|
|
998
|
+
throw _err("cmp/unauthorized-kga", "the certificate that signed the delivered key package does not chain to an accepted trust anchor (RFC 9483 sec. 4.1.6)");
|
|
999
|
+
}
|
|
1000
|
+
var parsedSigner;
|
|
1001
|
+
try { parsedSigner = x509.parse(signerCert); }
|
|
1002
|
+
catch (e) { throw _err("cmp/unauthorized-kga", "the certificate that signed the delivered key package could not be read", e); }
|
|
1003
|
+
if (!pkix.assertsPurpose(NS, parsedSigner, OID_CM_KGA)) {
|
|
1004
|
+
throw _err("cmp/unauthorized-kga", "the certificate that signed the delivered key package does not assert the id-kp-cmKGA extended key usage, which is what a key generation authority uses to show it is one (RFC 9483 sec. 4.1.6, RFC 9480 sec. 2.2)");
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function _signedDataVerifyOpts(opts) {
|
|
1009
|
+
if (opts.exempt) return {};
|
|
1010
|
+
var v = { trustAnchors: opts.trustAnchors, requiredEku: [OID_CM_KGA] };
|
|
1011
|
+
if (opts.intermediates != null) v.certs = opts.intermediates;
|
|
1012
|
+
if (opts.time != null) v.time = opts.time;
|
|
1013
|
+
return v;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** @internal The certificate options here take one certificate or a list of them, as DER or PEM, which
|
|
1017
|
+
* is the shape `pki.cmp.verify` takes. `pki.cms.verify` takes `certs` as a list of DER, so the pool is
|
|
1018
|
+
* normalized at the door: a single certificate forwarded unwrapped would be walked as a sequence of
|
|
1019
|
+
* its own bytes and contribute nothing, and a PEM string would not be read at all. */
|
|
1020
|
+
function _fixPoolCerts(v, label) {
|
|
1021
|
+
if (v == null) return v;
|
|
1022
|
+
var list = _fixCertList(_isArray(v) ? v : [v], label);
|
|
1023
|
+
return _map(list, function (c, i) {
|
|
1024
|
+
if (typeof c !== "string") return c;
|
|
1025
|
+
try { return x509.pemDecode(c); }
|
|
1026
|
+
catch (e) { throw _err("cmp/bad-input", "opts." + label + "[" + i + "] PEM could not be decoded", e); }
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/** @internal Both CMS layers arrive in either of two forms, and the two are structurally exclusive so
|
|
1031
|
+
* neither is guessed at. A ContentInfo opens with an OBJECT IDENTIFIER naming its content type; the
|
|
1032
|
+
* bare structure opens with its own `CMSVersion` INTEGER. Both forms are seen in practice. A CMP
|
|
1033
|
+
* `EncryptedKey` carries the `EnvelopedData` itself (RFC 9810 sec. 5.2.2), and RFC 5652 sec. 6.1
|
|
1034
|
+
* encrypts the CONTENT rather than a ContentInfo around it, so the plaintext under an `id-signedData`
|
|
1035
|
+
* `encryptedContentInfo` is a `SignedData`. Each is wrapped so one code path reads both. Anything
|
|
1036
|
+
* else is left alone for the ContentInfo parse to refuse by name. */
|
|
1037
|
+
function _asContentInfo(der, contentTypeOid, what) {
|
|
1038
|
+
var node;
|
|
1039
|
+
try { node = asn1.decode(der); }
|
|
1040
|
+
catch (e) { throw _keyPackageErr(what + " is not DER", e); }
|
|
1041
|
+
var first = node.constructed && node.children ? node.children[0] : null;
|
|
1042
|
+
if (!first || first.tagClass !== "universal" || first.tagNumber !== asn1.TAGS.INTEGER) return der;
|
|
1043
|
+
return b.sequence([b.oid(contentTypeOid), b.explicit(0, b.raw(der))]);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function _coerceKeyPackageBytes(container) {
|
|
1047
|
+
if (typeof container === "string") {
|
|
1048
|
+
try { return cms.pemDecode(container); }
|
|
1049
|
+
catch (e) { throw _err("cmp/bad-input", "the delivered key package container PEM could not be decoded", e); }
|
|
1050
|
+
}
|
|
1051
|
+
return container;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* @primitive pki.cmp.openKeyPackage
|
|
1056
|
+
* @signature pki.cmp.openKeyPackage(container, opts) -> Promise<{ keys, kga, trusted }>
|
|
1057
|
+
* @since 0.6.52
|
|
1058
|
+
* @status stable
|
|
1059
|
+
* @spec RFC 9483 sec. 4.1.6, RFC 5958, RFC 5652, RFC 9480 sec. 2.2
|
|
1060
|
+
* @defends unauthorized-key-generation-authority (CWE-863)
|
|
1061
|
+
* @related pki.cmp.session, pki.cms.decrypt, pki.cms.verify
|
|
1062
|
+
*
|
|
1063
|
+
* Open a centrally generated private key delivered in a CMP response (RFC 9483 sec. 4.1.6). A CA that
|
|
1064
|
+
* generates the key pair on the end entity's behalf puts it in the `privateKey` field of the granted
|
|
1065
|
+
* `CertifiedKeyPair`: an RFC 5958 `AsymmetricKeyPackage`, signed by the Key Generation Authority in a
|
|
1066
|
+
* CMS `SignedData`, sealed to this entity in a CMS `EnvelopedData`. `container` is those
|
|
1067
|
+
* `EnvelopedData` bytes (DER `Buffer` or PEM), which `pki.cmp.session` surfaces as
|
|
1068
|
+
* `deliveredKey` when the session is told to accept one.
|
|
1069
|
+
*
|
|
1070
|
+
* The two layers are undone in order and the authority is authorized BEFORE any key material is
|
|
1071
|
+
* returned. Which key management technique opens the container follows the protection of the request
|
|
1072
|
+
* that asked for the key: `opts.key` (with `opts.cert`) for the key transport and key agreement
|
|
1073
|
+
* techniques a signature-protected request selects (sec. 4.1.6.1, sec. 4.1.6.2), `opts.password` for
|
|
1074
|
+
* the password technique a MAC-protected request selects (sec. 4.1.6.3).
|
|
1075
|
+
*
|
|
1076
|
+
* Authorization is the point of the verb. The section requires the signer's certificate to carry the
|
|
1077
|
+
* `id-kp-cmKGA` extended key usage "in order to be accepted by the EE as a legitimate key generation
|
|
1078
|
+
* authority", so a certificate that merely PERMITS the purpose by carrying no `extendedKeyUsage` at
|
|
1079
|
+
* all does not authorize: the assertion must be present, and the certificate must chain to an anchor
|
|
1080
|
+
* in `opts.trustAnchors`. The one exception is the section's own: an entity that protected its request
|
|
1081
|
+
* with a shared secret "MAY omit the validation" and authorize the authority by that secret instead.
|
|
1082
|
+
* That is stated with `opts.authorizedBySharedSecret`, it applies only to a container opened with
|
|
1083
|
+
* `opts.password`, and the result then reports `trusted: false`.
|
|
1084
|
+
*
|
|
1085
|
+
* Returns `{ keys, kga, trusted }`. `keys` is every `OneAsymmetricKey` in the package, in order, each
|
|
1086
|
+
* as its own DER `Buffer` that is a PKCS#8 `PrivateKeyInfo` ready for `pki.key.import`. `kga` is the
|
|
1087
|
+
* signer certificate whose signature was checked. `trusted` says whether that certificate chained.
|
|
1088
|
+
* Malformed input, an unopenable container, an unverifiable signature, or an unauthorized authority
|
|
1089
|
+
* all throw a typed `CmpError`; no key material is returned on any of them.
|
|
1090
|
+
*
|
|
1091
|
+
* The container's shape is held to the profile where the shape decides who reads the key or what the
|
|
1092
|
+
* signature covers: it must be an `EnvelopedData`, it must name exactly one `RecipientInfo`, its
|
|
1093
|
+
* encrypted content type must be `id-signedData`, and the signed content type must be
|
|
1094
|
+
* `id-ct-KP-aKeyPackage`. The profile's CMS version literals are not enforced, because RFC 5652
|
|
1095
|
+
* sec. 5.1 and sec. 6.1 derive those from the signer and recipient forms in use and a conforming
|
|
1096
|
+
* sender reaches different values. Its choice-of-form rules, the signer identifier form and the
|
|
1097
|
+
* ordering of the certificates the SignedData carries, are not judged either. Both live inside the
|
|
1098
|
+
* encrypted SignedData rather than in the `container` bytes, and the result carries only the signer
|
|
1099
|
+
* certificate, so a caller holding a stricter profile cannot judge them from this verdict.
|
|
1100
|
+
*
|
|
1101
|
+
* `keys` is the whole RFC 5958 `AsymmetricKeyPackage`, which is `SIZE (1..MAX)`, so a package carrying
|
|
1102
|
+
* several is opened and every key returned. RFC 9483 sec. 4.1.6 profiles that to a sequence of one,
|
|
1103
|
+
* and where the count decides something it is held: a `pki.cmp.session` enrollment requires exactly
|
|
1104
|
+
* one, since one grant certifies one key and every further key is one the issued certificate says
|
|
1105
|
+
* nothing about. A caller using this verb on its own owns that binding.
|
|
1106
|
+
*
|
|
1107
|
+
* @opts
|
|
1108
|
+
* - `key` (Buffer|PEM|CryptoKey) -- this entity's private key, for a container opened by the key
|
|
1109
|
+
* transport or key agreement technique.
|
|
1110
|
+
* - `cert` (Buffer|PEM) -- the certificate for `key`. Optional: the profile fixes the recipient
|
|
1111
|
+
* count at one, so the arm is named rather than matched.
|
|
1112
|
+
* - `password` (string|Buffer) -- the shared secret, for a container opened by the password
|
|
1113
|
+
* technique. Copied at the door and wiped when the call returns.
|
|
1114
|
+
* - `trustAnchors` (Buffer|Buffer[]|PEM) -- the roots this entity accepts for the key generation
|
|
1115
|
+
* authority. REQUIRED unless `authorizedBySharedSecret` is set.
|
|
1116
|
+
* - `intermediates` (Buffer|Buffer[]|PEM) -- extra untrusted pool certificates for path building.
|
|
1117
|
+
* - `time` (Date) -- the validity instant for path validation. Defaults to the current time.
|
|
1118
|
+
* - `authorizedBySharedSecret` (boolean) -- authorize the authority by the secret that protected
|
|
1119
|
+
* the request rather than by a chain, which sec. 4.1.6 permits a MAC-protected exchange. Requires
|
|
1120
|
+
* `opts.password` and refuses `opts.trustAnchors`.
|
|
1121
|
+
* @example
|
|
1122
|
+
* var nb = new Date("2026-01-01T00:00:00Z"), na = new Date("2036-01-01T00:00:00Z");
|
|
1123
|
+
* var kgaPair = await pki.key.generate("Ed25519"); // the key generation authority
|
|
1124
|
+
* var kgaKey = await pki.key.export(kgaPair.privateKey);
|
|
1125
|
+
* var kgaCert = await pki.x509.sign({ subject: "Key Generation Authority",
|
|
1126
|
+
* subjectPublicKey: await pki.key.export(kgaPair.publicKey), notBefore: nb, notAfter: na,
|
|
1127
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["digitalSignature", "keyCertSign"],
|
|
1128
|
+
* extendedKeyUsage: ["cmKGA"], subjectKeyIdentifier: true } }, { key: kgaKey });
|
|
1129
|
+
* var rsa = { name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" };
|
|
1130
|
+
* var eePair = await pki.key.generate(rsa); // the enrolling end entity
|
|
1131
|
+
* var eeKey = await pki.key.export(eePair.privateKey);
|
|
1132
|
+
* var eeCert = await pki.x509.sign({ subject: "Enrolling client",
|
|
1133
|
+
* subjectPublicKey: await pki.key.export(eePair.publicKey), notBefore: nb, notAfter: na,
|
|
1134
|
+
* extensions: { keyUsage: ["keyEncipherment"] } }, { key: eeKey });
|
|
1135
|
+
* var generated = await pki.key.export((await pki.key.generate("Ed25519")).privateKey);
|
|
1136
|
+
* var signedPackage = await pki.cms.sign(pki.asn1.build.sequence([pki.asn1.build.raw(generated)]),
|
|
1137
|
+
* [{ key: kgaKey, cert: kgaCert }], { eContentType: "aKeyPackage", sid: "ski" });
|
|
1138
|
+
* var deliveredKeyDer = await pki.cms.encrypt(signedPackage, [{ cert: eeCert }],
|
|
1139
|
+
* { contentEncryptionAlgorithm: "aes-256-cbc", contentType: "signedData" });
|
|
1140
|
+
* var opened = await pki.cmp.openKeyPackage(deliveredKeyDer,
|
|
1141
|
+
* { key: eeKey, cert: eeCert, trustAnchors: [kgaCert] });
|
|
1142
|
+
* opened.keys.length; // 1
|
|
1143
|
+
* var privateKey = await pki.key.import(opened.keys[0]);
|
|
1144
|
+
*/
|
|
1145
|
+
function openKeyPackage(container, opts) {
|
|
1146
|
+
var made = [];
|
|
1147
|
+
return guard.async.deferred(async function () {
|
|
1148
|
+
try {
|
|
1149
|
+
return await _openKeyPackage(container, _fixOpenKeyPackageOptions(opts, made));
|
|
1150
|
+
} finally {
|
|
1151
|
+
guard.secret.zeroizeAll(made, CmpError, "cmp/bad-input", "the copied key package shared secret");
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
843
1156
|
/**
|
|
844
1157
|
* @primitive pki.cmp.verify
|
|
845
1158
|
* @signature pki.cmp.verify(message, opts?) -> Promise<verdict>
|
|
@@ -929,6 +1242,7 @@ module.exports = {
|
|
|
929
1242
|
transfer: cmpBuild.transfer,
|
|
930
1243
|
wellKnownUrl: cmpBuild.wellKnownUrl,
|
|
931
1244
|
verify: verify,
|
|
1245
|
+
openKeyPackage: openKeyPackage,
|
|
932
1246
|
setEngine: setEngine,
|
|
933
1247
|
senderBoundToCert: _senderBoundToCert,
|
|
934
1248
|
};
|
package/lib/guard-name.js
CHANGED
|
@@ -332,6 +332,55 @@ function _hostConstraintBody(s) {
|
|
|
332
332
|
return _charAt(h, 0) === "." ? _strSlice(h, 1) : h;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/** @internal A label with nothing in it, which is what makes a subtree base unmatchable. The
|
|
336
|
+
* comparisons in lib/path-validate.js compare a base against a name by suffix, so a base carrying an
|
|
337
|
+
* empty label asks for a name carrying one, and a name that carries one is not a name. */
|
|
338
|
+
function _hasEmptyLabel(s) {
|
|
339
|
+
if (s.length === 0) return true;
|
|
340
|
+
if (_charAt(s, 0) === "." || _charAt(s, s.length - 1) === ".") return true;
|
|
341
|
+
return _indexOf(s, "..") !== -1;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** @internal Whether the comparison can read a constraint base at all, which is a narrower question
|
|
345
|
+
* than whether a caller may state one. The door holds a caller to a well-formed host name; the
|
|
346
|
+
* comparison asks only whether an empty label leaves the base matching no name, and answers such a
|
|
347
|
+
* base with no verdict rather than with no match, since an excluded subtree that matches nothing
|
|
348
|
+
* excludes nothing. The two differ in both directions. A label carrying an underscore or edged with a
|
|
349
|
+
* hyphen is not a host name the door accepts, and the suffix comparison still applies it to exactly
|
|
350
|
+
* the names it names, so it stays enforced. The reduction here is the comparison's own, so the two
|
|
351
|
+
* cannot part company over a base like "..", whose body is empty only once a leading dot the
|
|
352
|
+
* comparison keeps has been taken off.
|
|
353
|
+
*
|
|
354
|
+
* The rule follows the comparison's own branches rather than the tag alone, since one tag can be
|
|
355
|
+
* compared two ways. An rfc822Name base carrying an at-sign is matched exactly and is read as a
|
|
356
|
+
* mailbox; one without an at-sign constrains the host and is read as a subtree, as a dNSName base is.
|
|
357
|
+
*
|
|
358
|
+
* A base that reduces to nothing is read by tag, because the comparisons do not agree on it. For
|
|
359
|
+
* dNSName it names the root of the namespace and `hostConstraintMatch` matches every name, so it is
|
|
360
|
+
* readable. For rfc822Name the same reduction leaves no domain to compare and `emailMatch` matches no
|
|
361
|
+
* mailbox, and for uniformResourceIdentifier `uriMatch` reads no host, so both are unreadable.
|
|
362
|
+
* @enforced-by behavioral -- the rule has no rename-proof code shape, and the RED vectors in
|
|
363
|
+
* path-validate.test.js driving a CA's own nameConstraints extension are the guard. */
|
|
364
|
+
function constraintBaseUnreadable(tag, base) {
|
|
365
|
+
if (typeof base !== "string") return true;
|
|
366
|
+
/** @internal A base carrying an at-sign names one mailbox, and `emailMatch` compares it to a name
|
|
367
|
+
* exactly, local part and domain alike, rather than as a subtree. Each half is therefore read on
|
|
368
|
+
* its own terms: the local part by the RFC 5321 sec. 4.1.2 Dot-string or Quoted-string rule, which
|
|
369
|
+
* admits an underscore and refuses an empty atom, and the domain by the empty-label rule. A
|
|
370
|
+
* bracketed address literal carries no empty label in either accepted form, so the domain rule
|
|
371
|
+
* passes it through to the comparison, which reads it whole. */
|
|
372
|
+
if (tag === 1 && _lastIndexOf(base, "@") !== -1) {
|
|
373
|
+
var mb = splitMailbox(base);
|
|
374
|
+
if (mb === null || mb === "ambiguous") return true;
|
|
375
|
+
if (!_isMailboxLocal(mb[0])) return true;
|
|
376
|
+
return _hasEmptyLabel(stripTrailingDot(mb[1]));
|
|
377
|
+
}
|
|
378
|
+
var body = stripTrailingDot(base);
|
|
379
|
+
if (body === "") return tag !== 2;
|
|
380
|
+
if (_charAt(body, 0) === ".") body = _strSlice(body, 1);
|
|
381
|
+
return _hasEmptyLabel(body);
|
|
382
|
+
}
|
|
383
|
+
|
|
335
384
|
/** @internal Why a name-constraint base cannot be enforced, or null when it can. A base outside the
|
|
336
385
|
* form its tag names is not a narrower namespace: an empty dNSName base matches every name, and a
|
|
337
386
|
* base holding characters no host name can hold matches none, so the same value that reads as a
|
|
@@ -358,11 +407,19 @@ function constraintBaseRefusal(tag, base) {
|
|
|
358
407
|
return _isHostName(_hostConstraintBody(base)) ? null
|
|
359
408
|
: "must be a host name, optionally written as a subtree with a leading dot";
|
|
360
409
|
}
|
|
410
|
+
/** @internal RFC 5280 sec. 4.2.1.10 says of a URI constraint: "The constraint MUST be specified as
|
|
411
|
+
* a fully qualified domain name and MAY specify a host or a domain. Examples would be
|
|
412
|
+
* host.example.com and .example.com." Its dNSName paragraph states no such requirement, so a single
|
|
413
|
+
* label is a valid dNSName base and is not a valid URI base. A caller meets this rule at the door,
|
|
414
|
+
* whether through `pki.trust.anchor` or through the validator's subtree seeds. A base a certificate
|
|
415
|
+
* carries in its own extension never reaches a door, and `uriMatch` is what holds it to the rule
|
|
416
|
+
* there, answering `path/name-constraint-unsupported` for a host it cannot read as fully qualified.
|
|
417
|
+
* `constraintBaseUnreadable` does not judge label count and does not stand in for that check. */
|
|
361
418
|
if (tag === 6) {
|
|
362
419
|
var h = _hostConstraintBody(_toLowerCase(base));
|
|
363
420
|
var dotted = _isHostName(h) && _indexOf(h, ".") !== -1 && !_allCharsIn(h, _DIGDOT_TABLE);
|
|
364
421
|
return dotted ? null
|
|
365
|
-
: "must be a
|
|
422
|
+
: "must be a fully qualified domain name, which RFC 5280 sec. 4.2.1.10 requires of a URI constraint and not of a dNSName one";
|
|
366
423
|
}
|
|
367
424
|
return null;
|
|
368
425
|
}
|
|
@@ -473,4 +530,5 @@ module.exports = intrinsic.freeze({
|
|
|
473
530
|
lowerAscii: lowerAscii,
|
|
474
531
|
stripTrailingDot: stripTrailingDot, isFqdnHost: isFqdnHost, splitMailbox: splitMailbox,
|
|
475
532
|
constraintBaseRefusal: constraintBaseRefusal,
|
|
533
|
+
constraintBaseUnreadable: constraintBaseUnreadable,
|
|
476
534
|
});
|
package/lib/guard-parsed.js
CHANGED
|
@@ -193,6 +193,22 @@ function isRecordedAsProduced(obj) {
|
|
|
193
193
|
return true;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The bytes a recorded parse result came from, for a consumer that has to hand DER on to a verb taking
|
|
198
|
+
* bytes rather than a parsed object. A copy, so the caller cannot reach the recorded source.
|
|
199
|
+
*
|
|
200
|
+
* Null for anything not recorded, and for a result whose fields no longer match what the parse
|
|
201
|
+
* produced: the bytes are the truth, so handing them on for an object a caller has since edited would
|
|
202
|
+
* silently discard the edit and forward something the caller did not mean.
|
|
203
|
+
*
|
|
204
|
+
* @enforced-by behavioral -- an accessor has no rename-proof shape to detect; its contract is pinned by
|
|
205
|
+
* the vectors that drive it on an unrecorded object, a recorded one, and an edited one.
|
|
206
|
+
*/
|
|
207
|
+
function sourceOf(obj) {
|
|
208
|
+
if (!isRecordedAsProduced(obj)) return null;
|
|
209
|
+
return _bufferFrom(_weakGet(PROVENANCE, obj).source);
|
|
210
|
+
}
|
|
211
|
+
|
|
196
212
|
function _shapeOf(obj) {
|
|
197
213
|
var shape = _create(null);
|
|
198
214
|
var keys = _allNames(obj);
|
|
@@ -278,5 +294,5 @@ module.exports = _freezeExports({
|
|
|
278
294
|
fromTrustedSource: fromTrustedSource, recordingParser: recordingParser,
|
|
279
295
|
recordingWalker: recordingWalker,
|
|
280
296
|
isCert: certShape, isCrl: crlShape, isRecorded: isRecorded,
|
|
281
|
-
isRecordedAsProduced: isRecordedAsProduced,
|
|
297
|
+
isRecordedAsProduced: isRecordedAsProduced, sourceOf: sourceOf,
|
|
282
298
|
});
|
package/lib/key.js
CHANGED
|
@@ -340,6 +340,164 @@ async function publicFromPrivate(privateKey, opts) {
|
|
|
340
340
|
return opts.pem ? pkix.pemEncode(spki, "PUBLIC KEY", PemError) : spki;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* @internal Whether a private key and a SubjectPublicKeyInfo are the two halves of one key pair, for a
|
|
345
|
+
* caller deciding whether a private key it was handed goes with a certificate it was handed.
|
|
346
|
+
*
|
|
347
|
+
* Deriving the public key and comparing does NOT answer this. A key structure states its own public
|
|
348
|
+
* half and the key engine reads what it is told: RFC 5958 sec. 2 gives a OneAsymmetricKey an optional
|
|
349
|
+
* `publicKey`, an EC key's RFC 5915 `ECPrivateKey` carries its own public point, and an RSA private
|
|
350
|
+
* key carries the modulus and public exponent outright. A structure whose private components were
|
|
351
|
+
* replaced while its stored public ones were left alone therefore derives to exactly the public key
|
|
352
|
+
* that was planted in it, and the comparison accepts a private key that cannot use the certificate.
|
|
353
|
+
*
|
|
354
|
+
* So the pair is proven by USING it, on whichever operation the key type can perform: a signature the
|
|
355
|
+
* public half verifies, a Diffie-Hellman agreement that reaches the same secret from both sides, or a
|
|
356
|
+
* key encapsulation the private half decapsulates to the same secret. Each fails for a key that merely
|
|
357
|
+
* claims the public half.
|
|
358
|
+
*
|
|
359
|
+
* What a true answer means is exactly that: the private key WORKS with this public key, on the
|
|
360
|
+
* operation its type performs. It is not a statement that every field of the private structure is
|
|
361
|
+
* internally consistent, which no operation reveals. A component the algorithm ignores, such as a bit
|
|
362
|
+
* an X25519 scalar clamps away or an ML-KEM implicit-rejection secret, is not exercised by a
|
|
363
|
+
* correspondence that succeeds, and does not stop the key from being usable with the certificate.
|
|
364
|
+
* A caller needing structural consistency has a different question, and this is not it.
|
|
365
|
+
*/
|
|
366
|
+
var _PAIR_DIGEST = Object.assign(Object.create(null), { rsa: "sha256", ec: "sha256" });
|
|
367
|
+
var _PAIR_AGREE = Object.assign(Object.create(null), { x25519: 1, x448: 1 });
|
|
368
|
+
var _PAIR_KEM = Object.assign(Object.create(null),
|
|
369
|
+
{ "ml-kem-512": 1, "ml-kem-768": 1, "ml-kem-1024": 1 });
|
|
370
|
+
/** @internal RFC 4055 sec. 1.2: ONE RSA key pair may be identified either by `rsaEncryption` or by
|
|
371
|
+
* `id-RSASSA-PSS`, and a delivered private key and the certificate that certifies it need not have
|
|
372
|
+
* chosen the same one. The key engine reports those two encodings as different types, so comparing
|
|
373
|
+
* types alone would call a usable pair no pair at all. They name one algorithm and are exercised
|
|
374
|
+
* together, under PSS, which is the padding the restricted encoding permits. */
|
|
375
|
+
var _PAIR_RSA = Object.assign(Object.create(null), { rsa: 1, "rsa-pss": 1 });
|
|
376
|
+
var _PAIR_PROBE = Buffer.from("pki.js key pair correspondence", "utf8");
|
|
377
|
+
|
|
378
|
+
function _sameKeyFamily(priv, pub) {
|
|
379
|
+
if (priv.asymmetricKeyType === pub.asymmetricKeyType) return true;
|
|
380
|
+
return !!(_PAIR_RSA[guard.text.keyOf(priv.asymmetricKeyType)] &&
|
|
381
|
+
_PAIR_RSA[guard.text.keyOf(pub.asymmetricKeyType)]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** @internal How to exercise a signing pair: the digest, and any padding the encoding requires. An
|
|
385
|
+
* `id-RSASSA-PSS` key may pin the hash and salt length it is usable with, and the engine refuses any
|
|
386
|
+
* other, so a pinned value from either half is what proves the pair. */
|
|
387
|
+
function _pairSignOptions(priv, pub) {
|
|
388
|
+
var privPss = priv.asymmetricKeyType === "rsa-pss", pubPss = pub.asymmetricKeyType === "rsa-pss";
|
|
389
|
+
if (!privPss && !pubPss) {
|
|
390
|
+
return { digest: _PAIR_DIGEST[guard.text.keyOf(priv.asymmetricKeyType)] || null, options: null };
|
|
391
|
+
}
|
|
392
|
+
var privDetails = _pssRestrictionOf(priv, privPss);
|
|
393
|
+
var pubDetails = _pssRestrictionOf(pub, pubPss);
|
|
394
|
+
/** @internal A half that pins nothing constrains nothing, so only two halves that BOTH pin can
|
|
395
|
+
* disagree. Both encodings pinning incompatible parameters describes no operation this pair can
|
|
396
|
+
* perform together, which is not the same as being different keys: it is reported as unexercisable
|
|
397
|
+
* rather than as a false "not a pair". Where one pins and the other does not, the pinned half
|
|
398
|
+
* supplies the parameters below and the engine enforces it against its own key. */
|
|
399
|
+
if (_pssConflict(privDetails, pubDetails)) return null;
|
|
400
|
+
var digest = privDetails.hashAlgorithm || pubDetails.hashAlgorithm || "sha256";
|
|
401
|
+
/** @internal The engine derives MGF1 from the digest and takes no separate option for it, so a half
|
|
402
|
+
* that pins a mask generator other than its own hash names an operation this pair cannot perform
|
|
403
|
+
* together. That is the same "cannot be decided" verdict two conflicting halves get, not a false
|
|
404
|
+
* report that the halves are different keys. */
|
|
405
|
+
var mgf1 = privDetails.mgf1HashAlgorithm || pubDetails.mgf1HashAlgorithm;
|
|
406
|
+
if (mgf1 != null && mgf1 !== digest) return null;
|
|
407
|
+
var salt = _pssSalt(privDetails.saltLength, pubDetails.saltLength);
|
|
408
|
+
/** @internal Null-prototype, because the engine READS this object by name. A plain object would
|
|
409
|
+
* let an inherited `saltLength` reach the signature as a restriction neither half stated. */
|
|
410
|
+
var options = Object.create(null);
|
|
411
|
+
options.padding = nodeCrypto.constants.RSA_PKCS1_PSS_PADDING;
|
|
412
|
+
if (salt != null) options.saltLength = salt;
|
|
413
|
+
return { digest: digest, options: options };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
var _PSS_PINS = ["hashAlgorithm", "mgf1HashAlgorithm", "saltLength"];
|
|
417
|
+
|
|
418
|
+
/** @internal What a half pins, as a null-prototype record carrying only what the key itself states.
|
|
419
|
+
* A half pins nothing when it is not an id-RSASSA-PSS key at all, and equally when it is one whose
|
|
420
|
+
* parameters are absent, which RFC 4055 sec. 3.1 leaves optional. Reading either through a plain
|
|
421
|
+
* object would let an inherited `saltLength` or `hashAlgorithm` answer as a restriction the key never
|
|
422
|
+
* carried, and refuse a genuine pair. */
|
|
423
|
+
function _pssRestrictionOf(key, isPss) {
|
|
424
|
+
var out = Object.create(null);
|
|
425
|
+
var details = isPss ? key.asymmetricKeyDetails : null;
|
|
426
|
+
if (details == null) return out;
|
|
427
|
+
for (var i = 0; i < _PSS_PINS.length; i++) {
|
|
428
|
+
var name = _PSS_PINS[i];
|
|
429
|
+
if (Object.prototype.hasOwnProperty.call(details, name) && details[name] != null) out[name] = details[name];
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** @internal Whether two halves state a restriction that cannot be satisfied at once. RFC 4055
|
|
435
|
+
* sec. 3.3 requires the signature's hash and mask generator to match the key's, so two halves naming
|
|
436
|
+
* different ones describe no shared operation. It treats the salt length differently: "The saltLength
|
|
437
|
+
* field in the signature parameters MUST be greater or equal to that in the key parameters field",
|
|
438
|
+
* and sec. 3.1 says the field "does not need to be fixed for a given RSA key pair". Two halves naming
|
|
439
|
+
* different salt lengths are therefore both satisfied by the larger, and do not conflict. A field
|
|
440
|
+
* only one half states constrains only that half, so it cannot disagree with anything. */
|
|
441
|
+
function _pssConflict(a, b) {
|
|
442
|
+
if (a.hashAlgorithm != null && b.hashAlgorithm != null && a.hashAlgorithm !== b.hashAlgorithm) return true;
|
|
443
|
+
if (a.mgf1HashAlgorithm != null && b.mgf1HashAlgorithm != null && a.mgf1HashAlgorithm !== b.mgf1HashAlgorithm) return true;
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** @internal The smallest salt length both halves accept, which is the larger of the two floors. */
|
|
448
|
+
function _pssSalt(a, b) {
|
|
449
|
+
if (a == null) return b;
|
|
450
|
+
if (b == null) return a;
|
|
451
|
+
return a > b ? a : b;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function correspondsTo(privateKey, spkiDer) {
|
|
455
|
+
var privDer = await _toPrivateKeyDer(privateKey);
|
|
456
|
+
var priv, pub;
|
|
457
|
+
try { priv = nodeCrypto.createPrivateKey({ key: privDer, format: "der", type: "pkcs8" }); }
|
|
458
|
+
catch (e) { throw _err("key/bad-input", "the private key could not be read", e); }
|
|
459
|
+
try { pub = nodeCrypto.createPublicKey({ key: Buffer.from(spkiDer), format: "der", type: "spki" }); }
|
|
460
|
+
catch (e) { throw _err("key/bad-input", "the public key could not be read", e); }
|
|
461
|
+
if (!_sameKeyFamily(priv, pub)) return false;
|
|
462
|
+
var kind = guard.text.keyOf(priv.asymmetricKeyType);
|
|
463
|
+
try {
|
|
464
|
+
if (_PAIR_KEM[kind]) {
|
|
465
|
+
var sealed = nodeCrypto.encapsulate(pub);
|
|
466
|
+
return guard.crypto.constantTimeEqual(nodeCrypto.decapsulate(priv, sealed.ciphertext), sealed.sharedKey);
|
|
467
|
+
}
|
|
468
|
+
if (_PAIR_AGREE[kind]) {
|
|
469
|
+
var ephemeral = nodeCrypto.generateKeyPairSync(priv.asymmetricKeyType);
|
|
470
|
+
if (!guard.crypto.constantTimeEqual(
|
|
471
|
+
nodeCrypto.diffieHellman({ privateKey: priv, publicKey: ephemeral.publicKey }),
|
|
472
|
+
nodeCrypto.diffieHellman({ privateKey: ephemeral.privateKey, publicKey: pub }))) return false;
|
|
473
|
+
/** @internal The agreement proves the SCALAR, not the encoding it was handed. RFC 7748 masks the
|
|
474
|
+
* top bit of an X25519 u-coordinate before the ladder, so a public value differing from the real
|
|
475
|
+
* one only in a masked bit reaches the same secret from both sides. The certificate would then
|
|
476
|
+
* carry bytes this key does not produce, and its subjectKeyIdentifier and any pinning of it
|
|
477
|
+
* would name a key the holder does not have. These types store no public copy in the private
|
|
478
|
+
* structure for an attacker to plant, so what the scalar generates IS the canonical answer and
|
|
479
|
+
* comparing against it adds a bound the agreement alone does not give. */
|
|
480
|
+
return guard.crypto.constantTimeEqual(
|
|
481
|
+
nodeCrypto.createPublicKey(priv).export({ format: "der", type: "spki" }),
|
|
482
|
+
pub.export({ format: "der", type: "spki" }));
|
|
483
|
+
}
|
|
484
|
+
var how = _pairSignOptions(priv, pub);
|
|
485
|
+
if (how === null) {
|
|
486
|
+
throw _err("key/unsupported-algorithm", "the two halves pin incompatible RSASSA-PSS parameters, so no operation exercises them together and whether they are a pair cannot be decided");
|
|
487
|
+
}
|
|
488
|
+
var signKey = how.options ? Object.assign(Object.create(null), { key: priv }, how.options) : priv;
|
|
489
|
+
var verifyKey = how.options ? Object.assign(Object.create(null), { key: pub }, how.options) : pub;
|
|
490
|
+
return nodeCrypto.verify(how.digest, _PAIR_PROBE, verifyKey,
|
|
491
|
+
nodeCrypto.sign(how.digest, _PAIR_PROBE, signKey)) === true;
|
|
492
|
+
} catch (e) {
|
|
493
|
+
/** @internal The incompatible-parameters verdict is decided rather than caught, so it is passed
|
|
494
|
+
* through with the reason it was raised for. Rewrapping it would tell an operator the pair could
|
|
495
|
+
* not be exercised without saying that the two encodings pin conflicting parameters. */
|
|
496
|
+
if (e && e.code === "key/unsupported-algorithm" && e.isPkiError === true) throw e;
|
|
497
|
+
throw _err("key/unsupported-algorithm", "the key pair could not be exercised to decide whether its two halves correspond", e);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
343
501
|
|
|
344
502
|
function _detectKeyInput(input) {
|
|
345
503
|
var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: KeyError, prefix: "key" });
|
|
@@ -397,4 +555,5 @@ module.exports = {
|
|
|
397
555
|
import: import_,
|
|
398
556
|
generate: generate,
|
|
399
557
|
publicFromPrivate: publicFromPrivate,
|
|
558
|
+
correspondsTo: correspondsTo, // @internal
|
|
400
559
|
};
|
package/lib/oid.js
CHANGED
|
@@ -174,6 +174,8 @@ var FAMILIES = Object.assign(Object.create(null), {
|
|
|
174
174
|
countersignature: 6, challengePassword: 7, extensionRequest: 14,
|
|
175
175
|
smimeCapabilities: 15, friendlyName: 20, localKeyId: 21 } },
|
|
176
176
|
|
|
177
|
+
keyPackageContentTypes: { base: [2, 16, 840, 1, 101, 2, 1, 2, 78], of: { aKeyPackage: 5 } },
|
|
178
|
+
|
|
177
179
|
pkcs9CertTypes: { base: [1, 2, 840, 113549, 1, 9, 22], of: { x509Certificate: 1, sdsiCertificate: 2 } },
|
|
178
180
|
pkcs9CrlTypes: { base: [1, 2, 840, 113549, 1, 9, 23], of: { x509CRL: 1 } },
|
|
179
181
|
|
package/lib/path-validate.js
CHANGED
|
@@ -486,8 +486,16 @@ function ipMatch(constraint, addr) {
|
|
|
486
486
|
return true;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
/** @internal The one place a constraint base meets a name, so the base's form is read here and
|
|
490
|
+
* nowhere else. A caller's base met `guard.name.constraintBaseRefusal` at the door; a base a
|
|
491
|
+
* certificate carries in its own nameConstraints extension met no door, and the same rule decides it
|
|
492
|
+
* here. Answering a base outside its tag's form with no match rather than no verdict would make an
|
|
493
|
+
* excluded subtree that covers nothing exclude nothing. */
|
|
489
494
|
function nameMatchesConstraint(gnTag, gnValue, base) {
|
|
490
495
|
if (base.tagNumber !== gnTag) return null;
|
|
496
|
+
if (gnTag === 1 || gnTag === 2 || gnTag === 6) {
|
|
497
|
+
if (guard.name.constraintBaseUnreadable(gnTag, base.value)) return "unsupported";
|
|
498
|
+
}
|
|
491
499
|
switch (gnTag) {
|
|
492
500
|
case 1: return emailMatch(base.value, gnValue);
|
|
493
501
|
case 2: return hostConstraintMatch(base.value, gnValue);
|