burnledger 0.6.0 → 0.7.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/cjs/verify.d.ts.map +1 -1
- package/dist/cjs/verify.js +109 -24
- package/dist/cjs/verify.js.map +1 -1
- package/dist/esm/cli.d.ts.map +1 -1
- package/dist/esm/cli.js +6 -2
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/verify.d.ts.map +1 -1
- package/dist/esm/verify.js +109 -24
- package/dist/esm/verify.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +6 -2
- package/src/verify.ts +135 -21
package/src/verify.ts
CHANGED
|
@@ -61,6 +61,18 @@ const FORMAT_VERSION_V6 = "6.0";
|
|
|
61
61
|
const PAYLOAD_TYPE_ATTESTATION_V7 = "burnledger.attestation.v7";
|
|
62
62
|
const PAYLOAD_TYPE_VERIFICATION_RECORD_V7 = "burnledger.verification_record.v7";
|
|
63
63
|
const FORMAT_VERSION_V7 = "7.0";
|
|
64
|
+
// v8 adds one per-system field, recoverable_state: whether anything checked for
|
|
65
|
+
// a restorable copy of what the record certifies gone. Its tags move for the
|
|
66
|
+
// reason every earlier version's did, and this time the bytes under them differ
|
|
67
|
+
// by that field. Everything v7 signs, v8 still signs.
|
|
68
|
+
//
|
|
69
|
+
// READ-ONLY IN THIS BUILD, mirroring core: nothing here issues a v8 record. A
|
|
70
|
+
// verifier that can read a format has to ship BEFORE anything signs one, or a
|
|
71
|
+
// genuine record reaches a holder whose SDK rebuilds it under the wrong domain
|
|
72
|
+
// and reports a forgery (runbook invariant 5).
|
|
73
|
+
const PAYLOAD_TYPE_ATTESTATION_V8 = "burnledger.attestation.v8";
|
|
74
|
+
const PAYLOAD_TYPE_VERIFICATION_RECORD_V8 = "burnledger.verification_record.v8";
|
|
75
|
+
const FORMAT_VERSION_V8 = "8.0";
|
|
64
76
|
const FORMAT_VERSION_V3 = "3.0";
|
|
65
77
|
|
|
66
78
|
/**
|
|
@@ -86,6 +98,7 @@ export const KNOWN_FORMAT_VERSIONS: readonly string[] = Object.freeze([
|
|
|
86
98
|
FORMAT_VERSION_V5,
|
|
87
99
|
FORMAT_VERSION_V6,
|
|
88
100
|
FORMAT_VERSION_V7,
|
|
101
|
+
FORMAT_VERSION_V8,
|
|
89
102
|
]);
|
|
90
103
|
|
|
91
104
|
/**
|
|
@@ -106,6 +119,52 @@ function checkFormatVersion(version: unknown): string {
|
|
|
106
119
|
}
|
|
107
120
|
return version;
|
|
108
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Is `version` the same format as `floor`, or a later one?
|
|
125
|
+
*
|
|
126
|
+
* Ordered by position in KNOWN_FORMAT_VERSIONS, never by string comparison:
|
|
127
|
+
* "10.0" sorts before "9.0". Port of core.formatAtLeast, and it exists for the
|
|
128
|
+
* reason the Go one does — four gates there were written as `v === "7.0"`, read
|
|
129
|
+
* by everyone as "7.0 and later", and behaved as "7.0 only". Three were live
|
|
130
|
+
* defects and one silently downgraded a security property the day v8 was
|
|
131
|
+
* defined.
|
|
132
|
+
*
|
|
133
|
+
* USE THIS, not equality, whenever the question is "does this format sign X".
|
|
134
|
+
* An unknown version answers false for every floor, which is safe only because
|
|
135
|
+
* checkFormatVersion has already refused it before any payload is rebuilt.
|
|
136
|
+
*/
|
|
137
|
+
function formatAtLeast(version: string, floor: string): boolean {
|
|
138
|
+
const vi = KNOWN_FORMAT_VERSIONS.indexOf(version);
|
|
139
|
+
const fi = KNOWN_FORMAT_VERSIONS.indexOf(floor);
|
|
140
|
+
return vi >= 0 && fi >= 0 && vi >= fi;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Ports of the core.SignatureCovers* predicates, one per signed field group. */
|
|
144
|
+
function signatureCoversEnclavePcr0(version: string): boolean {
|
|
145
|
+
return formatAtLeast(version, FORMAT_VERSION_V5);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function signatureCoversAlgorithm(version: string): boolean {
|
|
149
|
+
return formatAtLeast(version, FORMAT_VERSION_V7);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function signatureCoversMeasuredTransport(version: string): boolean {
|
|
153
|
+
return formatAtLeast(version, FORMAT_VERSION_V7);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Does this format sign the per-system recoverable_state?
|
|
158
|
+
*
|
|
159
|
+
* v8 only, and everything after it. Emitting the field for v7 or older would
|
|
160
|
+
* rebuild bytes no signer ever produced and reject every record already issued;
|
|
161
|
+
* omitting it for v8 rebuilds a v7-shaped subset and reports a genuine v8
|
|
162
|
+
* record as forged.
|
|
163
|
+
*/
|
|
164
|
+
function signatureCoversRecoverableState(version: string): boolean {
|
|
165
|
+
return formatAtLeast(version, FORMAT_VERSION_V8);
|
|
166
|
+
}
|
|
167
|
+
|
|
109
168
|
const PAYLOAD_TYPE_TREE_HEAD = "burnledger.sth.v3";
|
|
110
169
|
// The domain for a tree head that names its log. See buildTreeHeadPayload.
|
|
111
170
|
const PAYLOAD_TYPE_TREE_HEAD_V7 = "burnledger.sth.v7";
|
|
@@ -446,8 +505,9 @@ export async function verifyCertificate(
|
|
|
446
505
|
// Step 0b: can this SDK check the algorithm the record names? Asked before
|
|
447
506
|
// any signature, because verifying an Ed25519 signature over a record that
|
|
448
507
|
// says it was signed with something else answers a question nobody asked.
|
|
449
|
-
// Below v7 no record names one, so there is nothing to check
|
|
450
|
-
|
|
508
|
+
// Below v7 no record names one, so there is nothing to check; from v7 on
|
|
509
|
+
// every record does, which is why this asks "v7 or later" rather than "v7".
|
|
510
|
+
if (signatureCoversAlgorithm(formatVersion) && issuer.algorithm !== ALGORITHM_ED25519) {
|
|
451
511
|
throw new VerificationError(
|
|
452
512
|
`record names signature algorithm ${JSON.stringify(issuer.algorithm)}; ` +
|
|
453
513
|
`this version of the SDK verifies ${ALGORITHM_ED25519}. ` +
|
|
@@ -906,10 +966,16 @@ function buildAttestationPayload(
|
|
|
906
966
|
certFormatVersion: string,
|
|
907
967
|
): Uint8Array {
|
|
908
968
|
const attestedAt = formatTimestamp(att.attested_at);
|
|
909
|
-
// v7 signs two per-system measurements v6 leaves on a mutable row
|
|
910
|
-
// the record's OWN version:
|
|
911
|
-
//
|
|
912
|
-
|
|
969
|
+
// v7 signs two per-system measurements v6 leaves on a mutable row, and v8
|
|
970
|
+
// adds recoverable_state beside them. Both gated on the record's OWN version:
|
|
971
|
+
// emitting a field for an older format would rebuild bytes no signer ever
|
|
972
|
+
// produced and reject every record already issued.
|
|
973
|
+
//
|
|
974
|
+
// Predicates, not equalities. v8 signs everything v7 signs, so a `=== "7.0"`
|
|
975
|
+
// here would drop read_only_enforcement and transport_security out of a v8
|
|
976
|
+
// payload and call every genuine v8 record a forgery.
|
|
977
|
+
const coversMeasured = signatureCoversMeasuredTransport(certFormatVersion);
|
|
978
|
+
const coversRecoverable = signatureCoversRecoverableState(certFormatVersion);
|
|
913
979
|
const systems = (att.systems as Record<string, unknown>[]).map((s) => {
|
|
914
980
|
const sys: Record<string, unknown> = {
|
|
915
981
|
canonical_version: (s.canonical_version as string | null) ?? null,
|
|
@@ -925,16 +991,26 @@ function buildAttestationPayload(
|
|
|
925
991
|
system_id: s.system_id as string,
|
|
926
992
|
system_name: s.system_name as string,
|
|
927
993
|
};
|
|
928
|
-
if (
|
|
994
|
+
if (coversMeasured) {
|
|
929
995
|
sys.read_only_enforcement = requireMeasured(
|
|
930
996
|
s.read_only_enforcement,
|
|
931
997
|
"read_only_enforcement",
|
|
932
998
|
s.system_name,
|
|
999
|
+
certFormatVersion,
|
|
933
1000
|
);
|
|
934
1001
|
sys.transport_security = requireMeasured(
|
|
935
1002
|
s.transport_security,
|
|
936
1003
|
"transport_security",
|
|
937
1004
|
s.system_name,
|
|
1005
|
+
certFormatVersion,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
if (coversRecoverable) {
|
|
1009
|
+
sys.recoverable_state = requireMeasured(
|
|
1010
|
+
s.recoverable_state,
|
|
1011
|
+
"recoverable_state",
|
|
1012
|
+
s.system_name,
|
|
1013
|
+
certFormatVersion,
|
|
938
1014
|
);
|
|
939
1015
|
}
|
|
940
1016
|
return sys;
|
|
@@ -951,18 +1027,27 @@ function buildAttestationPayload(
|
|
|
951
1027
|
}
|
|
952
1028
|
|
|
953
1029
|
/**
|
|
954
|
-
* Read a
|
|
1030
|
+
* Read a signed measured field that MUST be a non-empty string.
|
|
955
1031
|
*
|
|
956
1032
|
* A missing or non-string value cannot be turned into `""` and canonicalized:
|
|
957
1033
|
* the signer never emits an empty measurement, so an empty one here would
|
|
958
1034
|
* rebuild bytes no signature covers and be reported as forgery. Refusing with a
|
|
959
1035
|
* document-shape message says the true thing — this record is malformed, not
|
|
960
1036
|
* this record is fake.
|
|
1037
|
+
*
|
|
1038
|
+
* The version is passed in rather than written into the message: it was the
|
|
1039
|
+
* literal "7.0" here, which would have told the holder of an incomplete v8
|
|
1040
|
+
* record that their v8 document was a v7 one.
|
|
961
1041
|
*/
|
|
962
|
-
function requireMeasured(
|
|
1042
|
+
function requireMeasured(
|
|
1043
|
+
value: unknown,
|
|
1044
|
+
field: string,
|
|
1045
|
+
systemName: unknown,
|
|
1046
|
+
version: string,
|
|
1047
|
+
): string {
|
|
963
1048
|
if (typeof value !== "string" || value === "") {
|
|
964
1049
|
throw new VerificationError(
|
|
965
|
-
`system ${JSON.stringify(systemName)}: ${field} is missing from a
|
|
1050
|
+
`system ${JSON.stringify(systemName)}: ${field} is missing from a ${version} record, ` +
|
|
966
1051
|
"which signs it; the document is incomplete rather than unverifiable",
|
|
967
1052
|
);
|
|
968
1053
|
}
|
|
@@ -972,6 +1057,7 @@ function requireMeasured(value: unknown, field: string, systemName: unknown): st
|
|
|
972
1057
|
/** Domain separator for the record itself, by certificate format version. */
|
|
973
1058
|
function certificatePayloadType(version: string): string {
|
|
974
1059
|
checkFormatVersion(version);
|
|
1060
|
+
if (version === FORMAT_VERSION_V8) return PAYLOAD_TYPE_VERIFICATION_RECORD_V8;
|
|
975
1061
|
if (version === FORMAT_VERSION_V7) return PAYLOAD_TYPE_VERIFICATION_RECORD_V7;
|
|
976
1062
|
if (version === FORMAT_VERSION_V6) return PAYLOAD_TYPE_VERIFICATION_RECORD_V6;
|
|
977
1063
|
if (version === FORMAT_VERSION_V5) return PAYLOAD_TYPE_CERTIFICATE_V5;
|
|
@@ -987,6 +1073,10 @@ function certificatePayloadType(version: string): string {
|
|
|
987
1073
|
*/
|
|
988
1074
|
function attestationPayloadType(version: string): string {
|
|
989
1075
|
checkFormatVersion(version);
|
|
1076
|
+
// Equality here, deliberately: each format has its OWN separator, so this is
|
|
1077
|
+
// a lookup rather than a "this version onward" question. v3 and v4 share one
|
|
1078
|
+
// because their attestation bytes are identical.
|
|
1079
|
+
if (version === FORMAT_VERSION_V8) return PAYLOAD_TYPE_ATTESTATION_V8;
|
|
990
1080
|
if (version === FORMAT_VERSION_V7) return PAYLOAD_TYPE_ATTESTATION_V7;
|
|
991
1081
|
if (version === FORMAT_VERSION_V6) return PAYLOAD_TYPE_ATTESTATION_V6;
|
|
992
1082
|
if (version === FORMAT_VERSION_V5) return PAYLOAD_TYPE_ATTESTATION_V5;
|
|
@@ -1002,7 +1092,8 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1002
1092
|
// and a verification list joined only on the human-editable system_name,
|
|
1003
1093
|
// which made a partial deletion indistinguishable from a complete one.
|
|
1004
1094
|
const version = cert.certificate_format_version as string;
|
|
1005
|
-
const
|
|
1095
|
+
const coversMeasured = signatureCoversMeasuredTransport(version);
|
|
1096
|
+
const coversRecoverable = signatureCoversRecoverableState(version);
|
|
1006
1097
|
const systems = ((cert.systems as Record<string, unknown>[]) ?? []).map((s) => {
|
|
1007
1098
|
const sys: Record<string, unknown> = {
|
|
1008
1099
|
attested_at: formatTimestamp(s.attested_at),
|
|
@@ -1017,16 +1108,26 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1017
1108
|
verified_at: formatTimestamp(s.verified_at),
|
|
1018
1109
|
verified_count: s.verified_count as number,
|
|
1019
1110
|
};
|
|
1020
|
-
if (
|
|
1111
|
+
if (coversMeasured) {
|
|
1021
1112
|
sys.read_only_enforcement = requireMeasured(
|
|
1022
1113
|
s.read_only_enforcement,
|
|
1023
1114
|
"read_only_enforcement",
|
|
1024
1115
|
s.system_name,
|
|
1116
|
+
version,
|
|
1025
1117
|
);
|
|
1026
1118
|
sys.transport_security = requireMeasured(
|
|
1027
1119
|
s.transport_security,
|
|
1028
1120
|
"transport_security",
|
|
1029
1121
|
s.system_name,
|
|
1122
|
+
version,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if (coversRecoverable) {
|
|
1126
|
+
sys.recoverable_state = requireMeasured(
|
|
1127
|
+
s.recoverable_state,
|
|
1128
|
+
"recoverable_state",
|
|
1129
|
+
s.system_name,
|
|
1130
|
+
version,
|
|
1030
1131
|
);
|
|
1031
1132
|
}
|
|
1032
1133
|
return sys;
|
|
@@ -1043,12 +1144,12 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1043
1144
|
// Fields added after v3 are gated on the certificate's OWN version. A v3
|
|
1044
1145
|
// certificate must reconstruct to the same bytes forever; reading these
|
|
1045
1146
|
// unconditionally would break every certificate already issued.
|
|
1046
|
-
// v6 carries v5's shape exactly, so it takes every gate v5 takes.
|
|
1047
|
-
//
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const isV4Plus = version
|
|
1147
|
+
// v6 carries v5's shape exactly, so it takes every gate v5 takes. Asking
|
|
1148
|
+
// "this version onward" rather than listing versions at each use is what
|
|
1149
|
+
// stops a new format from silently missing one: these were a chain of
|
|
1150
|
+
// equalities that had to be extended by hand for v6, then v7, then v8.
|
|
1151
|
+
const isV5Plus = formatAtLeast(version, FORMAT_VERSION_V5);
|
|
1152
|
+
const isV4Plus = formatAtLeast(version, FORMAT_VERSION_V4);
|
|
1052
1153
|
|
|
1053
1154
|
const issuerObj: Record<string, unknown> = {
|
|
1054
1155
|
key_id: issuer.key_id as string,
|
|
@@ -1058,15 +1159,24 @@ function buildCertificatePayload(cert: Record<string, unknown>): Uint8Array {
|
|
|
1058
1159
|
// v7 signs the algorithm, and does so unconditionally: unlike legal_entity
|
|
1059
1160
|
// and enclave_pcr0, "which scheme signed this" is never unknown to a signer,
|
|
1060
1161
|
// so a v7 record missing it is malformed rather than merely sparse.
|
|
1061
|
-
if (
|
|
1062
|
-
issuerObj.algorithm = requireMeasured(
|
|
1162
|
+
if (signatureCoversAlgorithm(version)) {
|
|
1163
|
+
issuerObj.algorithm = requireMeasured(
|
|
1164
|
+
issuer.algorithm,
|
|
1165
|
+
"issuer.algorithm",
|
|
1166
|
+
"issuer",
|
|
1167
|
+
version,
|
|
1168
|
+
);
|
|
1063
1169
|
}
|
|
1064
1170
|
if (isV4Plus && typeof issuer.legal_entity === "string" && issuer.legal_entity !== "") {
|
|
1065
1171
|
issuerObj.legal_entity = issuer.legal_entity;
|
|
1066
1172
|
}
|
|
1067
1173
|
// Signed from v5, and omitted when absent or empty: a build with no
|
|
1068
1174
|
// measurement signs none, and "" is a value a reader could mistake for one.
|
|
1069
|
-
if (
|
|
1175
|
+
if (
|
|
1176
|
+
signatureCoversEnclavePcr0(version) &&
|
|
1177
|
+
typeof issuer.enclave_pcr0 === "string" &&
|
|
1178
|
+
issuer.enclave_pcr0 !== ""
|
|
1179
|
+
) {
|
|
1070
1180
|
issuerObj.enclave_pcr0 = issuer.enclave_pcr0;
|
|
1071
1181
|
}
|
|
1072
1182
|
|
|
@@ -1144,6 +1254,10 @@ function attestationSystems(cert: Record<string, unknown>): Record<string, unkno
|
|
|
1144
1254
|
// reads them.
|
|
1145
1255
|
read_only_enforcement: s.read_only_enforcement,
|
|
1146
1256
|
transport_security: s.transport_security,
|
|
1257
|
+
// Same reasoning for v8. It is measured at attest time and carried
|
|
1258
|
+
// unchanged onto the outcome, so projecting it back is a copy; dropping it
|
|
1259
|
+
// here would make every genuine v8 record fail its attestation signature.
|
|
1260
|
+
recoverable_state: s.recoverable_state,
|
|
1147
1261
|
}));
|
|
1148
1262
|
}
|
|
1149
1263
|
|