diff --git a/graviola/Cargo.toml b/graviola/Cargo.toml index 58ab22e1..4c36f256 100644 --- a/graviola/Cargo.toml +++ b/graviola/Cargo.toml @@ -26,3 +26,7 @@ serde_json = "1" [[test]] name = "wycheproof" required-features = ["__internal_08eaf2eb"] + +[[test]] +name = "zeroing" +required-features = ["__internal_08eaf2eb"] diff --git a/graviola/src/high/ecdsa.rs b/graviola/src/high/ecdsa.rs index 0d2fd407..b54b2b91 100644 --- a/graviola/src/high/ecdsa.rs +++ b/graviola/src/high/ecdsa.rs @@ -7,7 +7,7 @@ use super::hash::{Hash, HashContext}; use super::hmac_drbg::HmacDrbg; use super::pkcs8; use crate::error::{Error, KeyFormatError}; -use crate::low::Entry; +use crate::low::{zeroise, Entry}; use crate::mid::rng::{RandomSource, SystemRandom}; pub struct SigningKey { @@ -121,8 +121,8 @@ impl SigningKey { } let hash = ctx.finish(); - let mut encoded_private_key = [0u8; MAX_SCALAR_LEN]; - let encoded_private_key = self.private_key.encode(&mut encoded_private_key)?; + let mut encoded_private_key_buf = [0u8; MAX_SCALAR_LEN]; + let encoded_private_key = self.private_key.encode(&mut encoded_private_key_buf)?; let e = hash_to_scalar::(hash.as_ref())?; let mut e_bytes = [0u8; MAX_SCALAR_LEN]; @@ -132,6 +132,7 @@ impl SigningKey { &e_bytes[..C::Scalar::LEN_BYTES], random, ); + zeroise(&mut encoded_private_key_buf); let (k, r) = loop { let k = C::generate_random_key(&mut rng)?; diff --git a/graviola/src/high/rsa.rs b/graviola/src/high/rsa.rs index 8aa364b5..daefcc41 100644 --- a/graviola/src/high/rsa.rs +++ b/graviola/src/high/rsa.rs @@ -127,11 +127,11 @@ impl RsaPrivateSigningKey { .map_err(Error::Asn1Error)?; let e = e.try_into().map_err(|_| Error::OutOfRange)?; - let p = PosInt::from_bytes(decoded.prime1.as_ref())?; - let q = PosInt::from_bytes(decoded.prime2.as_ref())?; - let dp = PosInt::from_bytes(decoded.exponent1.as_ref())?; - let dq = PosInt::from_bytes(decoded.exponent2.as_ref())?; - let iqmp = PosInt::from_bytes(decoded.coefficient.as_ref())?; + let p = PosInt::from_bytes(decoded.prime1.as_ref())?.into(); + let q = PosInt::from_bytes(decoded.prime2.as_ref())?.into(); + let dp = PosInt::from_bytes(decoded.exponent1.as_ref())?.into(); + let dq = PosInt::from_bytes(decoded.exponent2.as_ref())?.into(); + let iqmp = PosInt::from_bytes(decoded.coefficient.as_ref())?.into(); let priv_key = rsa_priv::RsaPrivateKey::new(p, q, dp, dq, iqmp, n, e)?; Ok(Self(priv_key)) diff --git a/graviola/src/low/aarch64/aes.rs b/graviola/src/low/aarch64/aes.rs index 8c0d1b2e..95cad2a1 100644 --- a/graviola/src/low/aarch64/aes.rs +++ b/graviola/src/low/aarch64/aes.rs @@ -8,6 +8,7 @@ // // cf. the x86_64 version, on which this one is based. +use crate::low; use core::arch::aarch64::*; pub(crate) enum AesKey { @@ -88,6 +89,12 @@ impl AesKey128 { } } +impl Drop for AesKey128 { + fn drop(&mut self) { + low::zeroise(&mut self.round_keys); + } +} + pub(crate) struct AesKey256 { round_keys: [uint8x16_t; 14 + 1], } @@ -131,6 +138,12 @@ impl AesKey256 { } } +impl Drop for AesKey256 { + fn drop(&mut self) { + low::zeroise(&mut self.round_keys); + } +} + fn zero() -> uint8x16_t { unsafe { vdupq_n_u8(0) } } diff --git a/graviola/src/low/aarch64/cpu.rs b/graviola/src/low/aarch64/cpu.rs index 765709f3..7d0b13d1 100644 --- a/graviola/src/low/aarch64/cpu.rs +++ b/graviola/src/low/aarch64/cpu.rs @@ -11,6 +11,34 @@ pub(crate) fn leave_cpu_state(old: u32) { dit::maybe_disable(old); } +/// Effectively memset(ptr, 0, len), but not visible to optimiser +pub(crate) fn zero_bytes(ptr: *mut u8, len: usize) { + unsafe { + core::arch::asm!( + " eor {zero}.16b, {zero}.16b, {zero}.16b", + // by-16 loop + " 2: cmp {len}, #16", + " blt 3f", + " st1 {{{zero}.16b}}, [{ptr}]", + " add {ptr}, {ptr}, #16", + " sub {len}, {len}, #16", + " b 2b", + // by-1 loop + " 3: subs {len}, {len}, #1", + " blt 4f", + " strb wzr, [{ptr}], #1", + " b 3b", + " 4: ", + + ptr = inout(reg) ptr => _, + len = inout(reg) len => _, + + // clobbers + zero = out(vreg) _, + ) + } +} + pub(crate) fn verify_cpu_features() { assert!( is_aarch64_feature_detected!("neon"), diff --git a/graviola/src/low/aarch64/ghash.rs b/graviola/src/low/aarch64/ghash.rs index 59f53acd..407a35dd 100644 --- a/graviola/src/low/aarch64/ghash.rs +++ b/graviola/src/low/aarch64/ghash.rs @@ -5,6 +5,7 @@ //! //! Based on the implementation in low/x86_64/ghash.rs +use crate::low; use core::arch::aarch64::*; use core::mem; @@ -42,6 +43,13 @@ impl GhashTable { } } +impl Drop for GhashTable { + fn drop(&mut self) { + low::zeroise(&mut self.powers); + low::zeroise(&mut self.powers_xor); + } +} + pub(crate) struct Ghash<'a> { table: &'a GhashTable, current: uint64x2_t, diff --git a/graviola/src/low/generic/zeroise.rs b/graviola/src/low/generic/zeroise.rs new file mode 100644 index 00000000..6a6640b6 --- /dev/null +++ b/graviola/src/low/generic/zeroise.rs @@ -0,0 +1,31 @@ +// Written for Graviola by Joe Birr-Pixton, 2024. +// SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0 + +use crate::low::zero_bytes; + +/// Writes zeroes over the whole of the `v` slice. +pub(crate) fn zeroise(v: &mut [T]) { + zero_bytes(v.as_mut_ptr().cast(), size_of_val(v)); +} + +/// Writes zeroes over the whole of the `v` value. +pub(crate) fn zeroise_value(v: &mut T) { + zero_bytes(v as *mut T as *mut _, size_of::()); +} + +/// Marker trait for types who have valid all-bits-zero values. +pub(crate) trait Zeroable {} + +impl Zeroable for u8 {} +impl Zeroable for u64 {} +impl Zeroable for usize {} + +#[cfg(target_arch = "x86_64")] +impl Zeroable for core::arch::x86_64::__m256i {} +#[cfg(target_arch = "x86_64")] +impl Zeroable for core::arch::x86_64::__m128i {} + +#[cfg(target_arch = "aarch64")] +impl Zeroable for core::arch::aarch64::uint8x16_t {} +#[cfg(target_arch = "aarch64")] +impl Zeroable for core::arch::aarch64::uint64x2_t {} diff --git a/graviola/src/low/mod.rs b/graviola/src/low/mod.rs index f34338fb..99091846 100644 --- a/graviola/src/low/mod.rs +++ b/graviola/src/low/mod.rs @@ -18,6 +18,7 @@ mod generic { pub(crate) mod poly1305; pub(super) mod sha256; pub(super) mod sha512; + pub(super) mod zeroise; } mod entry; @@ -27,7 +28,8 @@ pub(crate) use entry::Entry; pub(crate) use generic::blockwise::Blockwise; pub(crate) use generic::ct_equal::ct_equal; pub(crate) use generic::poly1305; -pub(crate) use posint::PosInt; +pub(crate) use generic::zeroise::{zeroise, zeroise_value}; +pub(crate) use posint::{PosInt, SecretPosInt}; #[cfg(test)] mod tests; @@ -36,7 +38,7 @@ cfg_if::cfg_if! { if #[cfg(target_arch = "x86_64")] { mod x86_64; - pub(crate) use x86_64::cpu::{enter_cpu_state, leave_cpu_state, verify_cpu_features}; + pub(crate) use x86_64::cpu::{enter_cpu_state, zero_bytes, leave_cpu_state, verify_cpu_features}; pub(crate) use x86_64::chacha20; pub(crate) use x86_64::aes::AesKey; pub(crate) use x86_64::aes_gcm; @@ -95,7 +97,7 @@ cfg_if::cfg_if! { } else if #[cfg(target_arch = "aarch64")] { mod aarch64; - pub(crate) use aarch64::cpu::{enter_cpu_state, leave_cpu_state, verify_cpu_features}; + pub(crate) use aarch64::cpu::{enter_cpu_state, zero_bytes, leave_cpu_state, verify_cpu_features}; pub(crate) use aarch64::aes::AesKey; pub(crate) use aarch64::bignum_add::bignum_add; pub(crate) use aarch64::bignum_add_p256::bignum_add_p256; diff --git a/graviola/src/low/posint.rs b/graviola/src/low/posint.rs index 1807fb89..34136c17 100644 --- a/graviola/src/low/posint.rs +++ b/graviola/src/low/posint.rs @@ -4,6 +4,8 @@ use crate::low; use crate::Error; +use core::ops::{Deref, DerefMut}; + #[derive(Clone, Debug)] pub(crate) struct PosInt { words: [u64; N], @@ -452,6 +454,7 @@ impl PosInt { } } + low::zeroise(&mut table); accum.from_montgomery(n) } @@ -489,6 +492,38 @@ impl PosInt { } } +/// A `SecretPosInt` is a `PosInt` containing long-term key material. +/// +/// It is zeroed on drop. +pub(crate) struct SecretPosInt(PosInt); + +impl From> for SecretPosInt { + fn from(pi: PosInt) -> Self { + Self(pi) + } +} + +impl Deref for SecretPosInt { + type Target = PosInt; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SecretPosInt { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Drop for SecretPosInt { + fn drop(&mut self) { + low::zeroise(self.as_mut_words()); + low::zeroise_value(&mut self.used); + } +} + #[derive(Debug)] struct BitsMsbFirstIter<'a> { words: &'a [u64], diff --git a/graviola/src/low/tests.rs b/graviola/src/low/tests.rs index d3588f04..18c6b5e1 100644 --- a/graviola/src/low/tests.rs +++ b/graviola/src/low/tests.rs @@ -16,6 +16,20 @@ fn bignum_mux() { bignum_mux_equiv(u64::MAX, &[1; 1], &[0; 1]); } +#[test] +fn zeroise() { + for n in 0..1024 { + zeroise_equiv(n); + } +} + +fn zeroise_equiv(len: usize) { + let expect = vec![0x00u8; len]; + let mut bytes = vec![0xffu8; len]; + super::zeroise(&mut bytes); + assert_eq!(expect, bytes); +} + mod model { pub fn bignum_mux(p: u64, z: &mut [u64], x_if_p: &[u64], y_if_not_p: &[u64]) { if p > 0 { diff --git a/graviola/src/low/x86_64/aes.rs b/graviola/src/low/x86_64/aes.rs index cb4b4408..97a9533d 100644 --- a/graviola/src/low/x86_64/aes.rs +++ b/graviola/src/low/x86_64/aes.rs @@ -7,6 +7,8 @@ use core::arch::x86_64::*; +use crate::low; + pub(crate) enum AesKey { Aes128(AesKey128), Aes256(AesKey256), @@ -70,6 +72,12 @@ impl AesKey128 { } } +impl Drop for AesKey128 { + fn drop(&mut self) { + low::zeroise(&mut self.round_keys); + } +} + fn zero() -> __m128i { unsafe { _mm_setzero_si128() } } @@ -94,6 +102,12 @@ impl AesKey256 { } } +impl Drop for AesKey256 { + fn drop(&mut self) { + low::zeroise(&mut self.round_keys); + } +} + macro_rules! expand_128 { ($rcon:literal, $t1:ident, $out:expr) => { // with [X3, _, X1, _] = t1 diff --git a/graviola/src/low/x86_64/cpu.rs b/graviola/src/low/x86_64/cpu.rs index 1f3f71cb..c1b69d7a 100644 --- a/graviola/src/low/x86_64/cpu.rs +++ b/graviola/src/low/x86_64/cpu.rs @@ -36,6 +36,38 @@ pub(crate) fn leave_cpu_state(_old: u32) { } } +/// Effectively memset(ptr, 0, len), but not visible to optimiser +pub(crate) fn zero_bytes(ptr: *mut u8, len: usize) { + unsafe { _zero_bytes(ptr, len) } +} + +#[target_feature(enable = "avx")] +unsafe fn _zero_bytes(ptr: *mut u8, len: usize) { + core::arch::asm!( + " vpxor {zero}, {zero}, {zero}", + // by-32 loop + " 2: cmp {len}, 32", + " jl 3f", + " vmovdqu [{ptr}], {zero}", + " add {ptr}, 32", + " sub {len}, 32", + " jmp 2b", + // by-1 loop + " 3: sub {len}, 1", + " jl 4f", + " mov byte ptr [{ptr}], 0", + " add {ptr}, 1", + " jmp 3b", + " 4: ", + + ptr = inout(reg) ptr => _, + len = inout(reg) len => _, + + // clobbers + zero = out(ymm_reg) _, + ) +} + /// This macro interdicts is_x86_feature_detected to /// allow testability. macro_rules! have_cpu_feature { diff --git a/graviola/src/low/x86_64/ghash.rs b/graviola/src/low/x86_64/ghash.rs index bf6afc0e..3bb1d56c 100644 --- a/graviola/src/low/x86_64/ghash.rs +++ b/graviola/src/low/x86_64/ghash.rs @@ -10,8 +10,10 @@ use core::arch::x86_64::*; use core::mem; +use crate::low; + pub(crate) struct GhashTable { - /// H, H^2, H^3, H^4, ... H^7 + /// H, H^2, H^3, H^4, ... H^8 powers: [__m128i; 8], /// `powers_xor[i]` is `powers[i].lo64 ^ powers[i].hi64` @@ -41,6 +43,13 @@ impl GhashTable { } } +impl Drop for GhashTable { + fn drop(&mut self) { + low::zeroise(&mut self.powers); + low::zeroise(&mut self.powers_xor); + } +} + pub(crate) struct Ghash<'a> { pub(crate) table: &'a GhashTable, pub(crate) current: __m128i, diff --git a/graviola/src/mid/chacha20poly1305.rs b/graviola/src/mid/chacha20poly1305.rs index f8c32fa8..d7120683 100644 --- a/graviola/src/mid/chacha20poly1305.rs +++ b/graviola/src/mid/chacha20poly1305.rs @@ -3,7 +3,7 @@ use crate::low::chacha20::ChaCha20; use crate::low::poly1305::Poly1305; -use crate::low::{ct_equal, Entry}; +use crate::low::{ct_equal, zeroise, Entry}; use crate::Error; pub struct ChaCha20Poly1305 { @@ -96,6 +96,12 @@ impl ChaCha20Poly1305 { } } +impl Drop for ChaCha20Poly1305 { + fn drop(&mut self) { + zeroise(&mut self.key); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/graviola/src/mid/p256.rs b/graviola/src/mid/p256.rs index 2bfafeaf..c2548ac2 100644 --- a/graviola/src/mid/p256.rs +++ b/graviola/src/mid/p256.rs @@ -147,6 +147,12 @@ impl fmt::Debug for PrivateKey { pub struct SharedSecret(pub [u8; 32]); +impl Drop for SharedSecret { + fn drop(&mut self) { + low::zeroise(&mut self.0); + } +} + #[derive(Clone, Copy, Debug, Default)] struct AffineMontPoint { xy: [u64; 8], @@ -731,6 +737,12 @@ impl Scalar { } } +impl Drop for Scalar { + fn drop(&mut self) { + low::zeroise(&mut self.0); + } +} + #[cfg(test)] struct Bits<'a> { scalar: &'a Scalar, diff --git a/graviola/src/mid/p384.rs b/graviola/src/mid/p384.rs index 25e6f8b4..1d99ab02 100644 --- a/graviola/src/mid/p384.rs +++ b/graviola/src/mid/p384.rs @@ -146,6 +146,12 @@ impl fmt::Debug for PrivateKey { pub struct SharedSecret(pub [u8; 48]); +impl Drop for SharedSecret { + fn drop(&mut self) { + low::zeroise(&mut self.0); + } +} + #[derive(Clone, Copy, Debug, Default)] struct AffineMontPoint { xy: [u64; 12], @@ -652,6 +658,12 @@ impl Scalar { } } +impl Drop for Scalar { + fn drop(&mut self) { + low::zeroise(&mut self.0); + } +} + #[cfg(test)] struct Bits<'a> { scalar: &'a Scalar, diff --git a/graviola/src/mid/rsa_priv.rs b/graviola/src/mid/rsa_priv.rs index 15ee3ff0..ab7772c3 100644 --- a/graviola/src/mid/rsa_priv.rs +++ b/graviola/src/mid/rsa_priv.rs @@ -6,11 +6,12 @@ use crate::error::Error; use crate::low; pub(crate) struct RsaPrivateKey { + public: RsaPublicKey, + p: RsaPosIntModP, q: RsaPosIntModP, dp: RsaPosIntModP, dq: RsaPosIntModP, - public: RsaPublicKey, iqmp_mont: RsaPosIntModP, p_montifier: RsaPosIntModP, @@ -40,18 +41,18 @@ impl RsaPrivateKey { } let public = RsaPublicKey::new(n, e)?; - let p_montifier = p.montifier(); - let q_montifier = q.montifier(); - let iqmp_mont = iqmp.to_montgomery(&p_montifier, &p); + let p_montifier: RsaPosIntModP = p.montifier().into(); + let q_montifier = q.montifier().into(); + let iqmp_mont = iqmp.to_montgomery(&p_montifier, &p).into(); let p0 = p.mont_neg_inverse(); let q0 = q.mont_neg_inverse(); Ok(Self { + public, p, q, dp, dq, - public, iqmp_mont, p_montifier, q_montifier, @@ -116,6 +117,13 @@ impl RsaPrivateKey { } } +impl Drop for RsaPrivateKey { + fn drop(&mut self) { + low::zeroise_value(&mut self.p0); + low::zeroise_value(&mut self.q0); + } +} + const MAX_PRIVATE_MODULUS_BITS: usize = 4096; const MAX_PRIVATE_MODULUS_WORDS: usize = MAX_PRIVATE_MODULUS_BITS / 64; pub(crate) const MAX_PRIVATE_MODULUS_BYTES: usize = MAX_PRIVATE_MODULUS_BITS / 8; @@ -123,5 +131,5 @@ pub(crate) const MAX_PRIVATE_MODULUS_BYTES: usize = MAX_PRIVATE_MODULUS_BITS / 8 const MIN_PRIVATE_MODULUS_BITS: usize = 1024; const MIN_PRIVATE_MODULUS_BYTES: usize = MIN_PRIVATE_MODULUS_BITS / 8; -type RsaPosIntModP = low::PosInt; +type RsaPosIntModP = low::SecretPosInt; type RsaPosIntModN = low::PosInt<{ MAX_PRIVATE_MODULUS_WORDS * 2 }>; diff --git a/graviola/src/mid/x25519.rs b/graviola/src/mid/x25519.rs index 84b98be3..efab959d 100644 --- a/graviola/src/mid/x25519.rs +++ b/graviola/src/mid/x25519.rs @@ -49,6 +49,12 @@ impl PrivateKey { } } +impl Drop for PrivateKey { + fn drop(&mut self) { + low::zeroise(&mut self.0 .0); + } +} + pub struct PublicKey(Array64x4); impl PublicKey { @@ -70,6 +76,12 @@ impl PublicKey { pub struct SharedSecret(pub [u8; 32]); +impl Drop for SharedSecret { + fn drop(&mut self) { + low::zeroise(&mut self.0); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/graviola/tests/zeroing.rs b/graviola/tests/zeroing.rs new file mode 100644 index 00000000..3869549a --- /dev/null +++ b/graviola/tests/zeroing.rs @@ -0,0 +1,132 @@ +//! Verification that long-term key data is zeroed. +//! +//! Some of this is subject to whims of the compiler, and +//! hand-written drop impls, so we test it here. +//! +//! Is this test sound? Well, it's a deliberate use-after-free, +//! so no. +//! +//! Does it work? If you're reading this, it has probably +//! broken, so also no. + +use core::ops::Deref; +use core::pin::Pin; +use core::ptr; + +#[test] +fn rsa() { + let rsa_priv = graviola::rsa::RsaPrivateSigningKey::from_pkcs1_der(include_bytes!( + "../src/high/rsa/rsa8192.der" + )) + .unwrap(); + let pub_key_size = size_of::(); + check_zeroed_on_drop_bounded(Box::pin(rsa_priv), Bounds::SkipPrefix(pub_key_size)); +} + +#[test] +fn ecdsa_p256() { + use graviola::ecdsa::*; + let ecdsa = + SigningKey::::from_pkcs8_der(include_bytes!("../src/high/ecdsa/secp256r1.pkcs8.der")) + .unwrap(); + check_zeroed_on_drop(Box::pin(ecdsa)); +} + +#[test] +fn ecdsa_p384() { + use graviola::ecdsa::*; + let ecdsa = + SigningKey::::from_pkcs8_der(include_bytes!("../src/high/ecdsa/secp384r1.pkcs8.der")) + .unwrap(); + check_zeroed_on_drop(Box::pin(ecdsa)); +} + +#[test] +fn ecdh_x25519() { + use graviola::x25519::PrivateKey; + let x25519 = PrivateKey::from_array(&[0xffu8; 32]); + check_zeroed_on_drop(Box::pin(x25519)); +} + +#[test] +fn ecdh_p256() { + use graviola::p256::PrivateKey; + let p256 = PrivateKey::from_bytes(&[0xefu8; 32]).unwrap(); + check_zeroed_on_drop(Box::pin(p256)); +} + +#[test] +fn ecdh_p384() { + use graviola::p384::PrivateKey; + let p384 = PrivateKey::from_bytes(&[0xefu8; 48]).unwrap(); + check_zeroed_on_drop(Box::pin(p384)); +} + +#[test] +fn aes_gcm() { + use graviola::aead::AesGcm; + + let aes128 = AesGcm::new(&[0xffu8; 16]); + check_zeroed_on_drop(Box::pin(aes128)); + + let aes256 = AesGcm::new(&[0xffu8; 32]); + check_zeroed_on_drop(Box::pin(aes256)); +} + +#[test] +fn chacha20_poly1305() { + use graviola::aead::ChaCha20Poly1305; + + let chacha = ChaCha20Poly1305::new([0xffu8; 32]); + check_zeroed_on_drop(Box::pin(chacha)); +} + +fn check_zeroed_on_drop(value: Pin>) { + check_zeroed_on_drop_bounded(value, Bounds::All) +} + +fn check_zeroed_on_drop_bounded(value: Pin>, bounds: Bounds) { + let ptr = value.deref() as *const T as *const u8; + let len = size_of::(); + assert_ne!(len, 0); + println!("this value is {len} bytes in length"); + let before_drop = read_into_vec(ptr, len); + drop(value); + let after_drop = read_into_vec(ptr, len); + + for i in bounds.start()..len { + if after_drop[i] != 0x00 { + println!("before_drop: {before_drop:02x?}"); + println!("after_drop: {after_drop:02x?}"); + panic!( + "byte {i} (0x{:x?}) was not cleared after drop", + after_drop[i] + ); + } + } +} + +fn read_into_vec(ptr: *const u8, len: usize) -> Vec { + let mut out = Vec::with_capacity(len); + + for i in 0..len { + // Safety: none + let byte = unsafe { ptr::read_volatile(ptr.add(i)) }; + out.push(byte); + } + out +} + +enum Bounds { + All, + SkipPrefix(usize), +} + +impl Bounds { + fn start(&self) -> usize { + match self { + Bounds::All => 8, // allow one word for heap metadata (lol, lmao) + Bounds::SkipPrefix(prefix) => *prefix, + } + } +}