Skip to content

Commit

Permalink
ospfv2: implement HMAC-SHA cryptographic authentication
Browse files Browse the repository at this point in the history
This commit introduces support for RFC 5709, which enables HMAC-SHA
cryptographic authentication in OSPFv2. Thanks to existing HMAC and
SHA crates, the implementation was made remarkably simple.

Interoperability tests were conducted successfully with the BIRD
routing stack. Unit tests were added for each HMAC-SHA variant.

Signed-off-by: Renato Westphal <renato@opensourcerouting.org>
  • Loading branch information
rwestphal committed Jun 22, 2023
1 parent dc733e6 commit 77cbfaf
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 31 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ enum-as-inner = "0.6"
futures = "0.3"
generational-arena = "0.2"
ipnetwork = "0.20"
hmac = "0.12"
itertools = "0.10"
libc = "0.2"
maplit = "1.0"
Expand All @@ -48,6 +49,8 @@ prost = "0.11"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
sha1 = "0.10"
sha2 = "0.10"
similar = "2.0"
socket2 = { version = "0.4", features = ["all"] }
tokio = { version = "1.0", features = ["full"] }
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Holo supports the following IETF RFCs and Internet drafts:
* RFC 5243 - OSPF Database Exchange Summary List Optimization
* RFC 5250 - The OSPF Opaque LSA Option
* RFC 5340 - OSPF for IPv6
* RFC 5709 - OSPFv2 HMAC-SHA Cryptographic Authentication
* RFC 5838 - Support of Address Families in OSPFv3
* RFC 6987 - OSPF Stub Router Advertisement
* RFC 7684 - OSPFv2 Prefix/Link Attribute Advertisement
Expand Down
3 changes: 3 additions & 0 deletions holo-ospf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ chrono.workspace = true
derive-new.workspace = true
enum-as-inner.workspace = true
generational-arena.workspace = true
hmac.workspace = true
ipnetwork.workspace = true
itertools.workspace = true
libc.workspace = true
Expand All @@ -26,6 +27,8 @@ num-traits.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
sha1.workspace = true
sha2.workspace = true
socket2.workspace = true
tokio.workspace = true
tracing.workspace = true
Expand Down
13 changes: 11 additions & 2 deletions holo-ospf/src/northbound/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,13 +837,22 @@ fn load_validation_callbacks_ospfv2() -> ValidationCallbacks {
ValidationCallbacksBuilder::new(core_cbs)
.path(ospf::areas::area::interfaces::interface::authentication::ospfv2_crypto_algorithm::PATH)
.validate(|args| {
let valid_options = [
CryptoAlgo::Md5.to_yang(),
CryptoAlgo::HmacSha1.to_yang(),
CryptoAlgo::HmacSha256.to_yang(),
CryptoAlgo::HmacSha384.to_yang(),
CryptoAlgo::HmacSha512.to_yang(),
];

let algo = args.dnode.get_string();
if algo != CryptoAlgo::Md5.to_yang() {
if !valid_options.iter().any(|option| *option == algo) {
return Err(format!(
"unsupported cryptographic algorithm (valid options: \"{}\")",
CryptoAlgo::Md5.to_yang()
valid_options.join(", "),
));
}

Ok(())
})
.build()
Expand Down
25 changes: 4 additions & 21 deletions holo-ospf/src/ospfv2/packet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use std::sync::atomic;
use bitflags::bitflags;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use holo_utils::bytes::{BytesExt, BytesMutExt, TLS_BUF};
use holo_utils::crypto::CryptoAlgo;
use holo_utils::ip::{AddressFamily, Ipv4AddrExt};
use internet_checksum::Checksum;
use num_derive::FromPrimitive;
Expand Down Expand Up @@ -833,16 +832,8 @@ impl PacketVersion<Self> for Ospfv2 {
[pkt_len as usize..pkt_len as usize + auth_len as usize];

// Compute message digest.
let digest = match auth.algo {
CryptoAlgo::Md5 => {
let data = &data[..pkt_len as usize];
auth::md5_digest(data, &auth.key)
}
_ => {
// Other algorithms can't be configured yet.
unreachable!()
}
};
let data = &data[..pkt_len as usize];
let digest = auth::message_digest(data, auth.algo, &auth.key);

// Check if the received message digest is valid.
if *auth_trailer != digest {
Expand All @@ -856,15 +847,7 @@ impl PacketVersion<Self> for Ospfv2 {
}

fn encode_auth_trailer(buf: &mut BytesMut, auth: &AuthCtx) {
match auth.algo {
CryptoAlgo::Md5 => {
let digest = auth::md5_digest(buf, &auth.key);
buf.put_slice(&digest);
}
_ => {
// Other algorithms can't be configured yet.
unreachable!()
}
}
let digest = auth::message_digest(buf, auth.algo, &auth.key);
buf.put_slice(&digest);
}
}
68 changes: 65 additions & 3 deletions holo-ospf/src/packet/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,28 @@
//

use std::sync::atomic::AtomicU32;
use std::sync::Arc;
use std::sync::{Arc, LazyLock as Lazy};

use derive_new::new;
use hmac::digest::block_buffer::Eager;
use hmac::digest::core_api::{
BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore, UpdateCore,
};
use hmac::digest::typenum::{IsLess, Le, NonZero, U256};
use hmac::digest::{HashMarker, Mac};
use hmac::Hmac;
use holo_utils::crypto::CryptoAlgo;
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Sha256, Sha384, Sha512};

pub static HMAC_APAD: Lazy<Vec<u8>> = Lazy::new(|| {
[0x87, 0x8F, 0xE1, 0xF3]
.into_iter()
.cycle()
.take(64)
.collect()
});

#[derive(Clone, Debug, new)]
#[derive(Deserialize, Serialize)]
Expand All @@ -24,9 +41,9 @@ pub struct AuthCtx {
pub seqno: Arc<AtomicU32>,
}

// ===== global functions =====
// ===== helper functions =====

pub(crate) fn md5_digest(data: &[u8], auth_key: &str) -> [u8; 16] {
fn md5_digest(data: &[u8], auth_key: &str) -> [u8; 16] {
// The authentication key needs to be 16-bytes long.
let mut auth_key = auth_key.as_bytes().to_vec();
auth_key.resize(16, 0);
Expand All @@ -36,3 +53,48 @@ pub(crate) fn md5_digest(data: &[u8], auth_key: &str) -> [u8; 16] {
ctx.consume(&auth_key);
*ctx.compute()
}

fn hmac_sha_digest<H>(
data: &[u8],
auth_key: &str,
digest_size: usize,
) -> Vec<u8>
where
H: CoreProxy,
H::Core: HashMarker
+ UpdateCore
+ FixedOutputCore
+ BufferKindUser<BufferKind = Eager>
+ Default
+ Clone,
<H::Core as BlockSizeUser>::BlockSize: IsLess<U256>,
Le<<H::Core as BlockSizeUser>::BlockSize, U256>: NonZero,
{
let mut mac = Hmac::<H>::new_from_slice(auth_key.as_bytes()).unwrap();
mac.update(data);
// TODO: infer digest size from the generic H type.
mac.update(&HMAC_APAD[..digest_size]);
let digest = mac.finalize();
digest.into_bytes().to_vec()
}

// ===== global functions =====

pub(crate) fn message_digest(
data: &[u8],
algo: CryptoAlgo,
key: &str,
) -> Vec<u8> {
let dsize = algo.digest_size() as usize;
match algo {
CryptoAlgo::Md5 => md5_digest(data, key).to_vec(),
CryptoAlgo::HmacSha1 => hmac_sha_digest::<Sha1>(data, key, dsize),
CryptoAlgo::HmacSha256 => hmac_sha_digest::<Sha256>(data, key, dsize),
CryptoAlgo::HmacSha384 => hmac_sha_digest::<Sha384>(data, key, dsize),
CryptoAlgo::HmacSha512 => hmac_sha_digest::<Sha512>(data, key, dsize),
_ => {
// Other algorithms can't be configured (e.g. Keyed SHA1).
unreachable!()
}
}
}
Loading

0 comments on commit 77cbfaf

Please sign in to comment.