@naughtbot/e2ee-payloads 0.9.0 → 0.10.0
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/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schema.d.ts +387 -1
- package/dist/schema.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.test.ts +268 -0
- package/src/index.ts +16 -0
- package/src/schema.ts +387 -1
package/src/index.test.ts
CHANGED
|
@@ -27,6 +27,12 @@ import type {
|
|
|
27
27
|
MailboxGpgDecryptResponseSuccessV1,
|
|
28
28
|
MailboxCaptchaRequestPayloadV1,
|
|
29
29
|
MailboxCaptchaResponsePayloadV1,
|
|
30
|
+
MailboxKeyInventoryApprovalBindingV1,
|
|
31
|
+
MailboxKeyInventoryEntryV1,
|
|
32
|
+
MailboxKeyInventoryRequestPayloadV1,
|
|
33
|
+
MailboxKeyInventoryResponsePayloadV1,
|
|
34
|
+
MailboxKeyInventoryResponseRejectedV1,
|
|
35
|
+
MailboxKeyInventoryResponseSharedV1,
|
|
30
36
|
MailboxLinkApprovalPayloadV1,
|
|
31
37
|
MailboxLinkRejectionPayloadV1,
|
|
32
38
|
MailboxLinkRequestPayloadV1,
|
|
@@ -37,6 +43,8 @@ import type {
|
|
|
37
43
|
MailboxSshSignResponsePayloadV1,
|
|
38
44
|
MailboxSshSignResponseSuccessV1,
|
|
39
45
|
NaughtBotApprovalBindingV1,
|
|
46
|
+
PublicKeyAlgorithm,
|
|
47
|
+
PublicKeyFormat,
|
|
40
48
|
} from "./index.ts";
|
|
41
49
|
|
|
42
50
|
const captchaApprovalBindingProfile =
|
|
@@ -815,3 +823,263 @@ describe("KeyPurpose", () => {
|
|
|
815
823
|
assert.ok(!(["ssh", "gpg", "age", "pkcs11"] as string[]).includes(bad));
|
|
816
824
|
});
|
|
817
825
|
});
|
|
826
|
+
|
|
827
|
+
interface KeyInventoryVectorsFixture {
|
|
828
|
+
canonical_owner: string;
|
|
829
|
+
approval_binding_format: string;
|
|
830
|
+
empty_key_list_digest: string;
|
|
831
|
+
key_list_digest: string;
|
|
832
|
+
canonical_keys_json: string;
|
|
833
|
+
canonical_binding_json: string;
|
|
834
|
+
binding: MailboxKeyInventoryApprovalBindingV1;
|
|
835
|
+
keys: MailboxKeyInventoryEntryV1[];
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function loadKeyInventoryVectors(): KeyInventoryVectorsFixture {
|
|
839
|
+
return JSON.parse(
|
|
840
|
+
readFileSync(
|
|
841
|
+
new URL("../../fixtures/key-inventory-v1-vectors.json", import.meta.url),
|
|
842
|
+
"utf8",
|
|
843
|
+
),
|
|
844
|
+
) as KeyInventoryVectorsFixture;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
describe("MailboxKeyInventoryRequestPayloadV1", () => {
|
|
848
|
+
it("round-trips a purpose-filtered request including pkcs11", () => {
|
|
849
|
+
const request: MailboxKeyInventoryRequestPayloadV1 = {
|
|
850
|
+
purposes: ["ssh", "gpg", "age", "pkcs11"],
|
|
851
|
+
source_info: { hostname: "work-laptop" },
|
|
852
|
+
};
|
|
853
|
+
const json = JSON.stringify(request);
|
|
854
|
+
assert.ok(json.includes('"purposes":["ssh","gpg","age","pkcs11"]'));
|
|
855
|
+
|
|
856
|
+
const parsed = JSON.parse(json) as MailboxKeyInventoryRequestPayloadV1;
|
|
857
|
+
assert.equal(parsed.purposes.length, 4);
|
|
858
|
+
assert.equal(parsed.purposes[3], "pkcs11");
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// Regression coverage for the closed canonical key-format enums. The
|
|
863
|
+
// PublicKeyAlgorithm / PublicKeyFormat unions are closed sets; an unknown
|
|
864
|
+
// member (e.g. "rsa" / "spki_der") must be a `tsc` error rather than a
|
|
865
|
+
// silent accept. Casting through `unknown` documents the intentionally bad
|
|
866
|
+
// runtime value.
|
|
867
|
+
describe("canonical key-format enums", () => {
|
|
868
|
+
it("PublicKeyAlgorithm is a closed set", () => {
|
|
869
|
+
const known: PublicKeyAlgorithm[] = ["ecdsa_p256", "ed25519", "x25519"];
|
|
870
|
+
for (const algorithm of known) {
|
|
871
|
+
assert.ok(["ecdsa_p256", "ed25519", "x25519"].includes(algorithm));
|
|
872
|
+
}
|
|
873
|
+
const bad = "rsa" as unknown as PublicKeyAlgorithm;
|
|
874
|
+
assert.ok(!(known as string[]).includes(bad));
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("PublicKeyFormat is a closed set", () => {
|
|
878
|
+
const known: PublicKeyFormat[] = ["sec1_compressed", "raw_32"];
|
|
879
|
+
for (const format of known) {
|
|
880
|
+
assert.ok(["sec1_compressed", "raw_32"].includes(format));
|
|
881
|
+
}
|
|
882
|
+
const bad = "spki_der" as unknown as PublicKeyFormat;
|
|
883
|
+
assert.ok(!(known as string[]).includes(bad));
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("rejects public_key_hex inconsistent with algorithm + format", () => {
|
|
887
|
+
// The schema carries the relaxed `^(02|03)?[0-9a-f]{64}$` pattern;
|
|
888
|
+
// receivers MUST additionally enforce the per-algorithm layout. This
|
|
889
|
+
// mirrors the Go validateCanonicalKeyMaterial helper.
|
|
890
|
+
const ok: MailboxKeyInventoryEntryV1 = {
|
|
891
|
+
purpose: "ssh",
|
|
892
|
+
id: "ssh-key-0001",
|
|
893
|
+
device_key_id: "se-handle-ssh-0001",
|
|
894
|
+
algorithm: "ecdsa_p256",
|
|
895
|
+
public_key_format: "sec1_compressed",
|
|
896
|
+
public_key_hex:
|
|
897
|
+
"02a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
|
|
898
|
+
};
|
|
899
|
+
assert.ok(canonicalKeyMaterialValid(ok));
|
|
900
|
+
|
|
901
|
+
// ecdsa_p256 with a 64-hex (raw_32-length) value is inconsistent.
|
|
902
|
+
assert.ok(
|
|
903
|
+
!canonicalKeyMaterialValid({
|
|
904
|
+
...ok,
|
|
905
|
+
public_key_hex:
|
|
906
|
+
"b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3",
|
|
907
|
+
}),
|
|
908
|
+
);
|
|
909
|
+
// ed25519 with a 66-hex (sec1-length) value is inconsistent.
|
|
910
|
+
assert.ok(
|
|
911
|
+
!canonicalKeyMaterialValid({
|
|
912
|
+
...ok,
|
|
913
|
+
algorithm: "ed25519",
|
|
914
|
+
public_key_format: "raw_32",
|
|
915
|
+
}),
|
|
916
|
+
);
|
|
917
|
+
// algorithm/format pairing mismatch.
|
|
918
|
+
assert.ok(
|
|
919
|
+
!canonicalKeyMaterialValid({ ...ok, public_key_format: "raw_32" }),
|
|
920
|
+
);
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
function canonicalKeyMaterialValid(entry: MailboxKeyInventoryEntryV1): boolean {
|
|
925
|
+
const hex = entry.public_key_hex;
|
|
926
|
+
switch (entry.algorithm) {
|
|
927
|
+
case "ecdsa_p256":
|
|
928
|
+
return (
|
|
929
|
+
entry.public_key_format === "sec1_compressed" &&
|
|
930
|
+
hex.length === 66 &&
|
|
931
|
+
(hex.startsWith("02") || hex.startsWith("03"))
|
|
932
|
+
);
|
|
933
|
+
case "ed25519":
|
|
934
|
+
case "x25519":
|
|
935
|
+
return entry.public_key_format === "raw_32" && hex.length === 64;
|
|
936
|
+
default:
|
|
937
|
+
return false;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
describe("MailboxKeyInventoryResponsePayloadV1", () => {
|
|
942
|
+
it("discriminates shared vs rejected via status", () => {
|
|
943
|
+
const fixture = loadKeyInventoryVectors();
|
|
944
|
+
|
|
945
|
+
const shared: MailboxKeyInventoryResponseSharedV1 = {
|
|
946
|
+
status: "shared",
|
|
947
|
+
request_envelope_id: "11111111-2222-4333-8444-555555555555",
|
|
948
|
+
keys: fixture.keys,
|
|
949
|
+
approval_binding: fixture.binding,
|
|
950
|
+
approval_binding_bytes: Buffer.from(
|
|
951
|
+
fixture.canonical_binding_json,
|
|
952
|
+
"utf8",
|
|
953
|
+
).toString("base64"),
|
|
954
|
+
approval_binding_format: "key-inventory-approval-binding/v1+json",
|
|
955
|
+
approval_signature: Buffer.from(
|
|
956
|
+
"key-inventory-approval-signature",
|
|
957
|
+
"utf8",
|
|
958
|
+
).toString("base64"),
|
|
959
|
+
approval_signature_algorithm: "ES256",
|
|
960
|
+
approval_proof: approvalProofFixture(),
|
|
961
|
+
};
|
|
962
|
+
const sharedResp = JSON.parse(
|
|
963
|
+
JSON.stringify(shared),
|
|
964
|
+
) as MailboxKeyInventoryResponsePayloadV1;
|
|
965
|
+
assert.equal(sharedResp.status, "shared");
|
|
966
|
+
if (sharedResp.status === "shared") {
|
|
967
|
+
assert.equal(sharedResp.keys.length, 4);
|
|
968
|
+
assert.equal(
|
|
969
|
+
Buffer.from(sharedResp.approval_binding_bytes, "base64").toString(
|
|
970
|
+
"utf8",
|
|
971
|
+
),
|
|
972
|
+
fixture.canonical_binding_json,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const rejected: MailboxKeyInventoryResponseRejectedV1 = {
|
|
977
|
+
status: "rejected",
|
|
978
|
+
request_envelope_id: "11111111-2222-4333-8444-555555555555",
|
|
979
|
+
error_code: 1,
|
|
980
|
+
error_message: "User rejected the key inventory request",
|
|
981
|
+
};
|
|
982
|
+
const rejectedResp = JSON.parse(
|
|
983
|
+
JSON.stringify(rejected),
|
|
984
|
+
) as MailboxKeyInventoryResponsePayloadV1;
|
|
985
|
+
assert.equal(rejectedResp.status, "rejected");
|
|
986
|
+
if (rejectedResp.status === "rejected") {
|
|
987
|
+
assert.equal(rejectedResp.error_code, 1);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
describe("MailboxKeyInventoryEntryV1 protocol metadata", () => {
|
|
993
|
+
it("round-trips SSH / GPG / age / PKCS#11 export metadata", () => {
|
|
994
|
+
const fixture = loadKeyInventoryVectors();
|
|
995
|
+
const [ssh, gpg, age, pkcs11] = fixture.keys;
|
|
996
|
+
|
|
997
|
+
const sshParsed = JSON.parse(
|
|
998
|
+
JSON.stringify(ssh),
|
|
999
|
+
) as MailboxKeyInventoryEntryV1;
|
|
1000
|
+
assert.equal(sshParsed.purpose, "ssh");
|
|
1001
|
+
assert.equal(
|
|
1002
|
+
sshParsed.ssh?.ssh_key_type,
|
|
1003
|
+
"sk-ecdsa-sha2-nistp256@openssh.com",
|
|
1004
|
+
);
|
|
1005
|
+
assert.ok(sshParsed.ssh?.ssh_fingerprint?.startsWith("SHA256:"));
|
|
1006
|
+
assert.equal(sshParsed.ssh?.ssh_sk_flags, 5);
|
|
1007
|
+
|
|
1008
|
+
const gpgParsed = JSON.parse(
|
|
1009
|
+
JSON.stringify(gpg),
|
|
1010
|
+
) as MailboxKeyInventoryEntryV1;
|
|
1011
|
+
assert.equal(
|
|
1012
|
+
gpgParsed.gpg?.fingerprint_hex,
|
|
1013
|
+
"A1D597197F5C1DACB3E3BB7862BF2FC536D562FF",
|
|
1014
|
+
);
|
|
1015
|
+
assert.ok(gpgParsed.gpg?.armored_public_key?.includes("PGP PUBLIC KEY"));
|
|
1016
|
+
assert.equal(
|
|
1017
|
+
gpgParsed.gpg?.encryption_public_key_hex,
|
|
1018
|
+
"03b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3",
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
const ageParsed = JSON.parse(
|
|
1022
|
+
JSON.stringify(age),
|
|
1023
|
+
) as MailboxKeyInventoryEntryV1;
|
|
1024
|
+
assert.ok(ageParsed.age?.age_recipient?.startsWith("age1nb1"));
|
|
1025
|
+
|
|
1026
|
+
const pkcs11Parsed = JSON.parse(
|
|
1027
|
+
JSON.stringify(pkcs11),
|
|
1028
|
+
) as MailboxKeyInventoryEntryV1;
|
|
1029
|
+
assert.equal(pkcs11Parsed.pkcs11?.cka_key_type, "CKK_EC");
|
|
1030
|
+
assert.equal(pkcs11Parsed.pkcs11?.can_sign, true);
|
|
1031
|
+
assert.equal(pkcs11Parsed.pkcs11?.can_derive, false);
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
describe("MailboxKeyInventoryApprovalBindingV1", () => {
|
|
1036
|
+
it("agrees with Go and Swift on the canonical key-inventory vectors", () => {
|
|
1037
|
+
const fixture = loadKeyInventoryVectors();
|
|
1038
|
+
assert.equal(
|
|
1039
|
+
fixture.canonical_owner,
|
|
1040
|
+
"NaughtBot/e2ee-payloads fixtures/key-inventory-v1-vectors.json",
|
|
1041
|
+
);
|
|
1042
|
+
assert.equal(fixture.keys.length, 4);
|
|
1043
|
+
|
|
1044
|
+
// SHA-256 over the canonical keys JSON MUST equal key_list_digest.
|
|
1045
|
+
const digest =
|
|
1046
|
+
"sha256:" +
|
|
1047
|
+
createHash("sha256")
|
|
1048
|
+
.update(fixture.canonical_keys_json, "utf8")
|
|
1049
|
+
.digest("hex");
|
|
1050
|
+
assert.equal(digest, fixture.key_list_digest);
|
|
1051
|
+
assert.equal(fixture.binding.key_list_digest, fixture.key_list_digest);
|
|
1052
|
+
|
|
1053
|
+
// The empty key list digest is SHA-256 of the JSON array "[]".
|
|
1054
|
+
const emptyDigest =
|
|
1055
|
+
"sha256:" + createHash("sha256").update("[]", "utf8").digest("hex");
|
|
1056
|
+
assert.equal(emptyDigest, fixture.empty_key_list_digest);
|
|
1057
|
+
|
|
1058
|
+
// Every fixture entry's canonical key material is internally consistent.
|
|
1059
|
+
for (const entry of fixture.keys) {
|
|
1060
|
+
assert.ok(
|
|
1061
|
+
canonicalKeyMaterialValid(entry),
|
|
1062
|
+
`fixture key ${entry.purpose} failed canonical validation`,
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Re-stringifying the fixture binding object (whose properties are
|
|
1067
|
+
// already in lexicographic order) reproduces the canonical signed bytes.
|
|
1068
|
+
assert.equal(
|
|
1069
|
+
JSON.stringify(fixture.binding),
|
|
1070
|
+
fixture.canonical_binding_json,
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
const bindingParsed = JSON.parse(
|
|
1074
|
+
JSON.stringify(fixture.binding),
|
|
1075
|
+
) as MailboxKeyInventoryApprovalBindingV1;
|
|
1076
|
+
assert.equal(bindingParsed.version, "key-inventory-approval-binding/v1");
|
|
1077
|
+
assert.equal(bindingParsed.request_envelope_type, "key_inventory_request");
|
|
1078
|
+
assert.deepEqual(bindingParsed.requested_purposes, [
|
|
1079
|
+
"ssh",
|
|
1080
|
+
"gpg",
|
|
1081
|
+
"age",
|
|
1082
|
+
"pkcs11",
|
|
1083
|
+
]);
|
|
1084
|
+
});
|
|
1085
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,11 @@ export type MailboxPkcs11DeriveResponsePayloadV1 = components["schemas"]["Mailbo
|
|
|
59
59
|
export type MailboxPkcs11DeriveResponseSuccessV1 = components["schemas"]["MailboxPkcs11DeriveResponseSuccessV1"];
|
|
60
60
|
export type MailboxPkcs11DeriveResponseFailureV1 = components["schemas"]["MailboxPkcs11DeriveResponseFailureV1"];
|
|
61
61
|
export type KeyPurpose = components["schemas"]["KeyPurpose"];
|
|
62
|
+
export type PublicKeyAlgorithm = components["schemas"]["PublicKeyAlgorithm"];
|
|
63
|
+
export type PublicKeyFormat = components["schemas"]["PublicKeyFormat"];
|
|
64
|
+
export type Sec1CompressedPublicKeyHex = components["schemas"]["Sec1CompressedPublicKeyHex"];
|
|
65
|
+
export type Raw32PublicKeyHex = components["schemas"]["Raw32PublicKeyHex"];
|
|
66
|
+
export type CanonicalPublicKeyHex = components["schemas"]["CanonicalPublicKeyHex"];
|
|
62
67
|
export type MailboxEnrollRequestPayloadV1 = components["schemas"]["MailboxEnrollRequestPayloadV1"];
|
|
63
68
|
export type MailboxEnrollResponsePayloadV1 = components["schemas"]["MailboxEnrollResponsePayloadV1"];
|
|
64
69
|
export type MailboxEnrollResponseApprovedV1 = components["schemas"]["MailboxEnrollResponseApprovedV1"];
|
|
@@ -86,3 +91,14 @@ export type MailboxFirstPartyPrivilegedActionRequestV1 = components["schemas"]["
|
|
|
86
91
|
export type MailboxFirstPartyRequestPayloadV1 = components["schemas"]["MailboxFirstPartyRequestPayloadV1"];
|
|
87
92
|
export type MailboxFirstPartyPrivilegedActionDecisionBindingV1 = components["schemas"]["MailboxFirstPartyPrivilegedActionDecisionBindingV1"];
|
|
88
93
|
export type MailboxFirstPartyResponsePayloadV1 = components["schemas"]["MailboxFirstPartyResponsePayloadV1"];
|
|
94
|
+
export type MailboxKeyInventoryRequestPayloadV1 = components["schemas"]["MailboxKeyInventoryRequestPayloadV1"];
|
|
95
|
+
export type MailboxKeyInventoryApprovalBindingFormat = components["schemas"]["MailboxKeyInventoryApprovalBindingFormat"];
|
|
96
|
+
export type MailboxKeyInventoryEntryV1 = components["schemas"]["MailboxKeyInventoryEntryV1"];
|
|
97
|
+
export type KeyInventorySshMetadataV1 = components["schemas"]["KeyInventorySshMetadataV1"];
|
|
98
|
+
export type KeyInventoryGpgMetadataV1 = components["schemas"]["KeyInventoryGpgMetadataV1"];
|
|
99
|
+
export type KeyInventoryAgeMetadataV1 = components["schemas"]["KeyInventoryAgeMetadataV1"];
|
|
100
|
+
export type KeyInventoryPkcs11MetadataV1 = components["schemas"]["KeyInventoryPkcs11MetadataV1"];
|
|
101
|
+
export type MailboxKeyInventoryResponseSharedV1 = components["schemas"]["MailboxKeyInventoryResponseSharedV1"];
|
|
102
|
+
export type MailboxKeyInventoryResponseRejectedV1 = components["schemas"]["MailboxKeyInventoryResponseRejectedV1"];
|
|
103
|
+
export type MailboxKeyInventoryResponsePayloadV1 = components["schemas"]["MailboxKeyInventoryResponsePayloadV1"];
|
|
104
|
+
export type MailboxKeyInventoryApprovalBindingV1 = components["schemas"]["MailboxKeyInventoryApprovalBindingV1"];
|