@amritk/nish-x86_64-linux 0.17.0 → 0.18.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.
@@ -56,6 +56,10 @@
56
56
  * does not yet, and `docs/security/quic.md` records that as QUIC-1. The
57
57
  * Initial keys are not secret, because anyone who reads the client's first
58
58
  * DCID can derive them (RFC 9001 §5.2); the Handshake and 1-RTT keys are.
59
+ * A connection slot derives into a `QuicKeysSlot` instead (`quicKeysInto`,
60
+ * `quicKeysUpdateInto`, `quicKeyUpdateSecretInto`, `quicInitialSecretsInto`):
61
+ * its keys live in arrays the slot owns, which `QuicKeysSlot.wipe` zeroes,
62
+ * and the derivation's own temporaries go with its arena block.
59
63
  *
60
64
  * **Constant time.** The AEADs and the header-protection masks are
61
65
  * `nish/crypto/aes` and `nish/crypto/chacha20poly1305`, with their guarantees.
@@ -73,10 +77,17 @@
73
77
  * private functions share the importing program's flat symbol namespace
74
78
  * (`docs/wp26-stdlib.md` §3e).
75
79
  */
76
- import { aesGcmOpen, aesGcmSeal, aesHeaderMask, aesKey, AesKey } from "nish/crypto/aes"
80
+ import { aesGcmOpen, aesGcmSeal, aesHeaderMask, aesKey, aesKeyInto, AesKey } from "nish/crypto/aes"
77
81
  import { chacha20HeaderMask, chacha20Poly1305Open, chacha20Poly1305Seal } from "nish/crypto/chacha20poly1305"
78
82
  import { timingSafeEqual } from "nish/crypto/ct"
79
- import { hkdfExpandLabelSha256, hkdfExpandLabelSha384, hkdfExtractSha256 } from "nish/crypto/hkdf"
83
+ import {
84
+ HkdfScratch,
85
+ hkdfExpandLabelInto,
86
+ hkdfExpandLabelSha256,
87
+ hkdfExpandLabelSha384,
88
+ hkdfExtractInto,
89
+ hkdfExtractSha256,
90
+ } from "nish/crypto/hkdf"
80
91
 
81
92
  /** QUIC version 1 (RFC 9000 §15), the only version this module protects. */
82
93
  export const QUIC_VERSION_1: i64 = 1
@@ -1046,6 +1057,219 @@ export const quicKeysUpdate = (keys: QuicKeys, nextSecret: u8[]): QuicKeys | nul
1046
1057
  return quicPacketDerive(keys.aead, nextSecret, keys.hp, keys.hpAes)
1047
1058
  }
1048
1059
 
1060
+ // ---- Keys in a connection slot ----------------------------------------------
1061
+
1062
+ /**
1063
+ * The storage one direction of one level's packet keys lives in for a
1064
+ * connection slot (QUIC-3): a key and a header-protection key of each
1065
+ * length, the IV, and an AES key schedule of each size for each, made once,
1066
+ * and the `QuicKeys` that `quicKeysInto` and `quicKeysUpdateInto` point at
1067
+ * the ones the negotiated AEAD uses. So installing a level's keys, or a key
1068
+ * update's, writes into memory the slot already holds and keeps nothing in
1069
+ * the arena, where `quicKeys` makes a fresh `QuicKeys` and four arrays a
1070
+ * level. `wipe` zeroes all of it.
1071
+ */
1072
+ export class QuicKeysSlot {
1073
+ /** The keys, pointing into the arrays below; `aead` is 0 until something is derived. */
1074
+ keys: QuicKeys
1075
+ key16: u8[]
1076
+ key32: u8[]
1077
+ hp16: u8[]
1078
+ hp32: u8[]
1079
+ packet128: AesKey
1080
+ packet256: AesKey
1081
+ hp128: AesKey
1082
+ hp256: AesKey
1083
+
1084
+ constructor() {
1085
+ this.key16 = new Array<u8>(16)
1086
+ this.key32 = new Array<u8>(32)
1087
+ this.hp16 = new Array<u8>(16)
1088
+ this.hp32 = new Array<u8>(32)
1089
+ this.keys = new QuicKeys(0, this.key16, new Array<u8>(QUIC_IV_SIZE), this.hp16)
1090
+ this.packet128 = new AesKey(10, new Array<u64>(88))
1091
+ this.packet256 = new AesKey(14, new Array<u64>(120))
1092
+ this.hp128 = new AesKey(10, new Array<u64>(88))
1093
+ this.hp256 = new AesKey(14, new Array<u64>(120))
1094
+ }
1095
+
1096
+ /**
1097
+ * Zeroes every key the slot holds: the keys and the IV with `secureZero`,
1098
+ * the AES schedules' `u64` words with ordinary stores, which stand because
1099
+ * the schedules stay reachable for the next derivation to fill.
1100
+ */
1101
+ wipe(): void {
1102
+ secureZero(this.key16)
1103
+ secureZero(this.key32)
1104
+ secureZero(this.hp16)
1105
+ secureZero(this.hp32)
1106
+ secureZero(this.keys.iv)
1107
+ quicPacketClearAes(this.packet128)
1108
+ quicPacketClearAes(this.packet256)
1109
+ quicPacketClearAes(this.hp128)
1110
+ quicPacketClearAes(this.hp256)
1111
+ }
1112
+ }
1113
+
1114
+ /** Zeroes an AES key schedule and its GCM hash key with ordinary stores (see `QuicKeysSlot.wipe`). */
1115
+ const quicPacketClearAes = (key: AesKey): void => {
1116
+ const words: u64[] = key.roundKeys
1117
+ for (let k: i32 = 0; k < toI32(words.length); k += 1) {
1118
+ words[k] = 0
1119
+ }
1120
+ key.hHi = 0
1121
+ key.hLo = 0
1122
+ }
1123
+
1124
+ /** Points `slot.keys` at the arrays `aead` needs: the packet key and its schedule, and the header-protection ones. */
1125
+ const quicPacketPoint = (slot: QuicKeysSlot, aead: i32): void => {
1126
+ const keys: QuicKeys = slot.keys
1127
+ const wide: boolean = quicPacketKeyLength(aead) === 32
1128
+ keys.aead = aead
1129
+ keys.key = wide ? slot.key32 : slot.key16
1130
+ keys.hp = wide ? slot.hp32 : slot.hp16
1131
+ keys.packetAes = null
1132
+ keys.hpAes = null
1133
+ if (aead !== QUIC_AEAD_CHACHA20_POLY1305) {
1134
+ keys.packetAes = wide ? slot.packet256 : slot.packet128
1135
+ keys.hpAes = wide ? slot.hp256 : slot.hp128
1136
+ }
1137
+ }
1138
+
1139
+ /**
1140
+ * `quicPacketDerive` in place: the packet key and IV from `secret` into
1141
+ * `keys`' own arrays, and for the AES suites the key's schedule expanded
1142
+ * into its own `AesKey`. HKDF runs in `kdf`, which wipes what it held, and
1143
+ * the expansion's temporaries go with the arena block.
1144
+ */
1145
+ const quicPacketDeriveInto = (kdf: HkdfScratch, keys: QuicKeys, secret: u8[]): void => {
1146
+ const aead: i32 = keys.aead
1147
+ const hashLength: i32 = quicPacketSecretLength(aead)
1148
+ using _scope = arena()
1149
+ const none: u8[] = []
1150
+ hkdfExpandLabelInto(kdf, hashLength, secret, "quic key", none, 0, 0, keys.key, 0, quicPacketKeyLength(aead))
1151
+ hkdfExpandLabelInto(kdf, hashLength, secret, "quic iv", none, 0, 0, keys.iv, 0, QUIC_IV_SIZE)
1152
+ const packetAes: AesKey | null = keys.packetAes
1153
+ if (packetAes !== null) {
1154
+ aesKeyInto(keys.key, packetAes)
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * `quicKeys` into `slot`: the packet key, IV and header-protection key of
1160
+ * RFC 9001 §5.1 derived from `secret` straight into the slot's arrays, in
1161
+ * `kdf`, with the AES schedules expanded in place. Answers the slot's keys,
1162
+ * or `null`, deriving nothing, where `quicKeys` answers `null`. The keys
1163
+ * are the slot's: the next derivation into it overwrites them.
1164
+ */
1165
+ export const quicKeysInto = (
1166
+ kdf: HkdfScratch,
1167
+ slot: QuicKeysSlot,
1168
+ aead: i32,
1169
+ secret: u8[]
1170
+ ): QuicKeys | null => {
1171
+ if (!quicPacketSecretFits(aead, secret)) {
1172
+ return null
1173
+ }
1174
+ quicPacketPoint(slot, aead)
1175
+ const keys: QuicKeys = slot.keys
1176
+ quicPacketDeriveInto(kdf, keys, secret)
1177
+ {
1178
+ using _scope = arena()
1179
+ const none: u8[] = []
1180
+ hkdfExpandLabelInto(
1181
+ kdf,
1182
+ quicPacketSecretLength(aead),
1183
+ secret,
1184
+ "quic hp",
1185
+ none,
1186
+ 0,
1187
+ 0,
1188
+ keys.hp,
1189
+ 0,
1190
+ quicPacketKeyLength(aead)
1191
+ )
1192
+ const hpAes: AesKey | null = keys.hpAes
1193
+ if (hpAes !== null) {
1194
+ aesKeyInto(keys.hp, hpAes)
1195
+ }
1196
+ }
1197
+ return keys
1198
+ }
1199
+
1200
+ /**
1201
+ * `quicKeysUpdate` into `slot`, which must not be the slot `keys` live in:
1202
+ * the next generation's packet key and IV from `nextSecret`, and `keys`'
1203
+ * header-protection key shared, as every generation shares it (RFC 9001
1204
+ * §6). Answers the slot's keys, or `null` where `quicKeysUpdate` does.
1205
+ */
1206
+ export const quicKeysUpdateInto = (
1207
+ kdf: HkdfScratch,
1208
+ slot: QuicKeysSlot,
1209
+ keys: QuicKeys,
1210
+ nextSecret: u8[]
1211
+ ): QuicKeys | null => {
1212
+ if (!quicPacketKeysUsable(keys) || !quicPacketSecretFits(keys.aead, nextSecret)) {
1213
+ return null
1214
+ }
1215
+ quicPacketPoint(slot, keys.aead)
1216
+ const next: QuicKeys = slot.keys
1217
+ next.hp = keys.hp
1218
+ next.hpAes = keys.hpAes
1219
+ quicPacketDeriveInto(kdf, next, nextSecret)
1220
+ return next
1221
+ }
1222
+
1223
+ /**
1224
+ * `quicKeyUpdateSecret` into `out`, which must be exactly the secret's
1225
+ * length and not `secret` itself. Answers whether it derived, which it does
1226
+ * where `quicKeyUpdateSecret` answers a secret.
1227
+ */
1228
+ export const quicKeyUpdateSecretInto = (kdf: HkdfScratch, aead: i32, secret: u8[], out: u8[]): boolean => {
1229
+ const length: i32 = quicPacketSecretLength(aead)
1230
+ if (!quicPacketSecretFits(aead, secret) || toI32(out.length) !== length) {
1231
+ return false
1232
+ }
1233
+ using _scope = arena()
1234
+ const none: u8[] = []
1235
+ hkdfExpandLabelInto(kdf, length, secret, "quic ku", none, 0, 0, out, 0, length)
1236
+ return true
1237
+ }
1238
+
1239
+ /**
1240
+ * `quicInitialSecrets` of the Destination Connection ID `buf[at .. at +
1241
+ * length)`, read in place, into `client` and `server`, 32 bytes each.
1242
+ * Answers whether it derived: not for an ID over 20 bytes, a window outside
1243
+ * `buf`, or an output of another length.
1244
+ */
1245
+ export const quicInitialSecretsInto = (
1246
+ kdf: HkdfScratch,
1247
+ buf: u8[],
1248
+ at: i32,
1249
+ length: i32,
1250
+ client: u8[],
1251
+ server: u8[]
1252
+ ): boolean => {
1253
+ if (
1254
+ length < 0 ||
1255
+ length > QUIC_MAX_CID_LENGTH ||
1256
+ at < 0 ||
1257
+ at > toI32(buf.length) - length ||
1258
+ toI32(client.length) !== 32 ||
1259
+ toI32(server.length) !== 32
1260
+ ) {
1261
+ return false
1262
+ }
1263
+ using _scope = arena()
1264
+ const none: u8[] = []
1265
+ const initial: u8[] = new Array<u8>(32)
1266
+ hkdfExtractInto(kdf, 32, quicPacketInitialSalt(), quicPacketSlice(buf, at, at + length), initial, 0)
1267
+ hkdfExpandLabelInto(kdf, 32, initial, "client in", none, 0, 0, client, 0, 32)
1268
+ hkdfExpandLabelInto(kdf, 32, initial, "server in", none, 0, 0, server, 0, 32)
1269
+ secureZero(initial)
1270
+ return true
1271
+ }
1272
+
1049
1273
  /** The bits of the first byte header protection covers: four in a long header, five in a short one (RFC 9001 §5.4.1). */
1050
1274
  const quicPacketProtectedBits = (first: i32): i32 => ((first & 0x80) !== 0 ? 0x0f : 0x1f)
1051
1275
 
@@ -1224,10 +1224,16 @@ export class QuicStreams {
1224
1224
  if (k >= 0 && k < count) {
1225
1225
  const stream: QuicStream = this.slots[k]
1226
1226
  if (stream.id >= 0 && stream.wantsToSend()) {
1227
- const next: i32 =
1228
- stream.resendLow >= 0 || stream.resendFin
1229
- ? this.putResend(stream, buf, at, end)
1230
- : this.putNew(stream, k, buf, at, end)
1227
+ let next: i32 = at
1228
+ if (stream.resendLow >= 0 || stream.resendFin) {
1229
+ next = this.putResend(stream, buf, at, end)
1230
+ }
1231
+ // A resend range acknowledged in full since it was queued is
1232
+ // cleared with nothing written: the new bytes go in its place
1233
+ // (H3-7), rather than a packet with none while they wait.
1234
+ if (next === at && stream.resendLow < 0 && !stream.resendFin) {
1235
+ next = this.putNew(stream, k, buf, at, end)
1236
+ }
1231
1237
  if (next > at) {
1232
1238
  return next
1233
1239
  }