use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use ring::signature::KeyPair;
use tokio::net::UdpSocket;
use dig_stun::credential::{
classify_request, decide, encode_challenge, encode_identity_request, encode_signed_request,
parse_challenge, query_reflexive_address_signed, signing_message, verify_signed_request,
CredentialError, CredentialMode, NonceCheck, NonceIssuer, RequestKind, ServerDecision,
SignedQueryError, StunSigner, ERR_BAD_REQUEST, ERR_STALE_NONCE, ERR_UNAUTHENTICATED, NONCE_LEN,
P256_SPKI_LEN, P256_SPKI_PREFIX,
};
use dig_stun::{parse_binding_request, StunError, TransactionId};
fn hex_to_bytes(hex: &str) -> Vec<u8> {
hex.split_whitespace()
.map(|b| u8::from_str_radix(b, 16).expect("valid hex byte"))
.collect()
}
const TXID: TransactionId = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
fn independent_spki_der() -> [u8; P256_SPKI_LEN] {
let bytes = hex_to_bytes(
"30 59 30 13 06 07 2a 86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 04 10 59 ec \
c6 97 1e 99 20 89 6f 53 f1 00 c4 cd 64 f3 b3 61 06 db a2 49 85 60 57 88 ad eb d0 03 80 68 \
fd cc 95 27 52 54 c9 51 78 91 30 99 23 33 6f 34 46 9f 48 ea 71 f4 f6 f1 cc 0d 8f c4 44 bb e4",
);
bytes.try_into().expect("91 bytes")
}
fn independent_nonce_wire() -> &'static [u8] {
b"AAABAgABAgMEBQYHCAkKCwwNDg8"
}
fn independent_sig_der() -> Vec<u8> {
hex_to_bytes(
"30 45 02 20 47 91 ce 4c 29 3b d1 3e fd d0 c9 32 03 a6 b5 ea ac 62 e2 e0 d4 b4 07 de c8 93 \
ea eb 4f d0 a2 14 02 21 00 98 03 e8 b7 02 8e 80 97 74 bb e3 59 31 ac c1 1c a0 12 19 bb c6 \
78 bf ab be 4b fd 4b 1a b0 d2 c3",
)
}
#[test]
fn p256_spki_prefix_matches_the_published_asn1_algorithm_identifier() {
let want = hex_to_bytes(
"30 59 30 13 06 07 2a 86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00",
);
assert_eq!(P256_SPKI_PREFIX.to_vec(), want);
assert_eq!(P256_SPKI_PREFIX.len(), 26);
}
#[test]
fn encode_challenge_bare_401_matches_independent_golden_vector() {
let got = encode_challenge(&TXID, ERR_UNAUTHENTICATED, None);
let want = hex_to_bytes(
"01 11 00 18 21 12 a4 42 00 01 02 03 04 05 06 07 08 09 0a 0b \
00 09 00 13 00 00 04 01 55 6e 61 75 74 68 65 6e 74 69 63 61 74 65 64 00",
);
assert_eq!(got, want);
assert_eq!(
got.len(),
44,
"bare refusal must be exactly 44 bytes (SPEC.md §14.3.3)"
);
}
#[test]
fn encode_challenge_401_with_nonce_matches_independent_golden_vector() {
let raw_nonce: [u8; NONCE_LEN] =
hex_to_bytes("00 00 01 02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f")
.try_into()
.unwrap();
let got = encode_challenge(&TXID, ERR_UNAUTHENTICATED, Some(&raw_nonce));
let want = hex_to_bytes(
"01 11 00 44 21 12 a4 42 00 01 02 03 04 05 06 07 08 09 0a 0b \
00 09 00 13 00 00 04 01 55 6e 61 75 74 68 65 6e 74 69 63 61 74 65 64 00 \
00 14 00 08 64 69 67 2d 73 74 75 6e \
00 15 00 1b 41 41 41 42 41 67 41 42 41 67 4d 45 42 51 59 48 43 41 6b 4b 43 77 77 4e 44 67 38 00",
);
assert_eq!(got, want);
assert_eq!(
got.len(),
88,
"a challenge must be exactly 88 bytes (SPEC.md §14.3.3)"
);
}
#[test]
fn encode_challenge_438_matches_independent_golden_vector() {
let raw_nonce: [u8; NONCE_LEN] =
hex_to_bytes("00 00 01 02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f")
.try_into()
.unwrap();
let got = encode_challenge(&TXID, ERR_STALE_NONCE, Some(&raw_nonce));
let want = hex_to_bytes(
"01 11 00 40 21 12 a4 42 00 01 02 03 04 05 06 07 08 09 0a 0b \
00 09 00 0f 00 00 04 26 53 74 61 6c 65 20 4e 6f 6e 63 65 00 \
00 14 00 08 64 69 67 2d 73 74 75 6e \
00 15 00 1b 41 41 41 42 41 67 41 42 41 67 4d 45 42 51 59 48 43 41 6b 4b 43 77 77 4e 44 67 38 00",
);
assert_eq!(got, want);
assert_eq!(
got.len(),
84,
"a stale-nonce response must be exactly 84 bytes (SPEC.md §14.3.3)"
);
}
#[test]
fn encode_challenge_400_matches_independent_golden_vector() {
let got = encode_challenge(&TXID, ERR_BAD_REQUEST, None);
let want = hex_to_bytes(
"01 11 00 14 21 12 a4 42 00 01 02 03 04 05 06 07 08 09 0a 0b \
00 09 00 0f 00 00 04 00 42 61 64 20 52 65 71 75 65 73 74 00",
);
assert_eq!(got, want);
assert_eq!(
got.len(),
40,
"a malformed refusal must be exactly 40 bytes (SPEC.md §14.3.3)"
);
}
#[test]
fn encode_identity_request_matches_independent_golden_vector() {
let spki = independent_spki_der();
let got = encode_identity_request(&TXID, &spki);
let want = hex_to_bytes(
"00 01 00 60 21 12 a4 42 00 01 02 03 04 05 06 07 08 09 0a 0b \
d1 60 00 5c 01 \
30 59 30 13 06 07 2a 86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 04 10 59 ec \
c6 97 1e 99 20 89 6f 53 f1 00 c4 cd 64 f3 b3 61 06 db a2 49 85 60 57 88 ad eb d0 03 80 68 \
fd cc 95 27 52 54 c9 51 78 91 30 99 23 33 6f 34 46 9f 48 ea 71 f4 f6 f1 cc 0d 8f c4 44 bb e4",
);
assert_eq!(got, want);
assert_eq!(
got.len(),
116,
"an identity request must be exactly 116 bytes (SPEC.md §14.9)"
);
}
#[test]
fn signing_message_matches_independent_computation() {
let spki = independent_spki_der();
let got = signing_message(&TXID, independent_nonce_wire(), &spki);
let want = hex_to_bytes(
"64 69 67 3a 73 74 75 6e 3a 76 31 01 00 01 02 03 04 05 06 07 08 09 0a 0b 00 1b \
41 41 41 42 41 67 41 42 41 67 4d 45 42 51 59 48 43 41 6b 4b 43 77 77 4e 44 67 38 \
30 59 30 13 06 07 2a 86 48 ce 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 04 10 59 ec \
c6 97 1e 99 20 89 6f 53 f1 00 c4 cd 64 f3 b3 61 06 db a2 49 85 60 57 88 ad eb d0 03 80 68 \
fd cc 95 27 52 54 c9 51 78 91 30 99 23 33 6f 34 46 9f 48 ea 71 f4 f6 f1 cc 0d 8f c4 44 bb e4",
);
assert_eq!(
got, want,
"the preimage must byte-match an independently computed one"
);
assert_eq!(got.len(), 144);
}
#[test]
fn a_signature_from_an_independent_library_verifies() {
let spki = independent_spki_der();
let sig = independent_sig_der();
let kind = RequestKind::Signed {
spki: &spki,
nonce: independent_nonce_wire(),
signature: &sig,
};
let identity =
verify_signed_request(&TXID, &kind).expect("an independently signed request must verify");
assert_eq!(identity.spki_der(), &spki);
}
#[test]
fn verify_signed_request_rejects_a_tampered_nonce() {
let spki = independent_spki_der();
let sig = independent_sig_der();
let mut tampered_nonce = independent_nonce_wire().to_vec();
tampered_nonce[0] ^= 1; let kind = RequestKind::Signed {
spki: &spki,
nonce: &tampered_nonce,
signature: &sig,
};
assert_eq!(
verify_signed_request(&TXID, &kind),
Err(CredentialError::BadSignature)
);
}
#[test]
fn verify_signed_request_rejects_wrong_kind_variants_without_panicking() {
assert_eq!(
verify_signed_request(&TXID, &RequestKind::Bare),
Err(CredentialError::Malformed)
);
let spki = independent_spki_der();
assert_eq!(
verify_signed_request(&TXID, &RequestKind::Identity { spki: &spki }),
Err(CredentialError::Malformed)
);
}
#[test]
fn verify_signed_request_rejects_a_malformed_spki_without_panicking() {
let too_short_spki = [0u8; 5];
let kind = RequestKind::Signed {
spki: &too_short_spki,
nonce: b"nonce",
signature: b"sig",
};
assert_eq!(
verify_signed_request(&TXID, &kind),
Err(CredentialError::Malformed)
);
let wrong_shape_spki = [0u8; P256_SPKI_LEN];
let kind = RequestKind::Signed {
spki: &wrong_shape_spki,
nonce: b"nonce",
signature: b"sig",
};
assert_eq!(
verify_signed_request(&TXID, &kind),
Err(CredentialError::Malformed)
);
}
#[test]
fn encode_signed_request_round_trips_through_classify_and_verify() {
let (signer, spki) = ring_test_signer();
let nonce = independent_nonce_wire();
let msg = encode_signed_request(&TXID, nonce, &signer);
assert!(
msg.len() <= 228,
"a signed request must never exceed 228 bytes (SPEC.md §14.9)"
);
let (got_txid, kind) =
classify_request(&msg).expect("a well-formed signed request must classify");
assert_eq!(got_txid, TXID);
let RequestKind::Signed {
spki: got_spki,
nonce: got_nonce,
..
} = kind
else {
panic!("expected RequestKind::Signed, got {kind:?}");
};
assert_eq!(got_spki, spki.as_slice());
assert_eq!(got_nonce, nonce);
let identity = verify_signed_request(&TXID, &kind).expect("the crate's own signer must verify");
assert_eq!(identity.spki_der().as_slice(), spki.as_slice());
}
#[test]
fn classify_request_recognizes_a_bare_request() {
let txid = [7u8; 12];
let msg = dig_stun::encode_binding_request(&txid);
let (got_txid, kind) = classify_request(&msg).unwrap();
assert_eq!(got_txid, txid);
assert_eq!(kind, RequestKind::Bare);
}
#[test]
fn classify_request_recognizes_an_identity_request() {
let spki = independent_spki_der();
let msg = encode_identity_request(&TXID, &spki);
let (got_txid, kind) = classify_request(&msg).unwrap();
assert_eq!(got_txid, TXID);
assert_eq!(kind, RequestKind::Identity { spki: &spki });
}
#[test]
fn classify_request_wraps_an_ordinary_stun_error() {
let err = classify_request(&[0u8; 4]).unwrap_err(); assert_eq!(err, CredentialError::Stun(StunError::Truncated));
}
#[test]
fn classify_request_rejects_a_signature_that_is_not_the_last_attribute() {
let (signer, _spki) = ring_test_signer();
let mut msg = encode_signed_request(&TXID, independent_nonce_wire(), &signer);
append_extra_attribute(&mut msg, 0x9999, &[0, 0, 0, 0]); assert_eq!(classify_request(&msg), Err(CredentialError::Malformed));
}
#[test]
fn classify_request_rejects_a_nonce_without_a_signature() {
let spki = independent_spki_der();
let identity_msg = encode_identity_request(&TXID, &spki);
let mut msg = identity_msg;
append_extra_attribute(
&mut msg,
dig_stun::credential::ATTR_NONCE,
independent_nonce_wire(),
);
assert_eq!(classify_request(&msg), Err(CredentialError::Malformed));
}
#[test]
fn classify_request_rejects_a_duplicated_identity_attribute() {
let spki = independent_spki_der();
let mut msg = encode_identity_request(&TXID, &spki);
let mut identity_value = vec![dig_stun::credential::CREDENTIAL_VERSION];
identity_value.extend_from_slice(&spki);
append_extra_attribute(
&mut msg,
dig_stun::credential::ATTR_DIG_IDENTITY,
&identity_value,
);
assert_eq!(classify_request(&msg), Err(CredentialError::Malformed));
}
#[test]
fn error_shapes_never_exceed_their_trigger_except_bare_refusal_at_stun_baseline() {
let spki = independent_spki_der();
let bare_request = dig_stun::encode_binding_request(&TXID);
let identity_request = encode_identity_request(&TXID, &spki);
let (signer, _spki) = ring_test_signer();
let signed_request = encode_signed_request(&TXID, independent_nonce_wire(), &signer);
let raw_nonce = [0u8; NONCE_LEN];
let bare_refusal = encode_challenge(&TXID, ERR_UNAUTHENTICATED, None);
let challenge = encode_challenge(&TXID, ERR_UNAUTHENTICATED, Some(&raw_nonce));
let stale = encode_challenge(&TXID, ERR_STALE_NONCE, Some(&raw_nonce));
let malformed = encode_challenge(&TXID, ERR_BAD_REQUEST, None);
assert_eq!(bare_refusal.len() as f64 / bare_request.len() as f64, 2.2);
assert!(challenge.len() < identity_request.len());
assert!(malformed.len() < identity_request.len());
assert!(stale.len() < signed_request.len());
}
fn append_extra_attribute(msg: &mut Vec<u8>, attr_type: u16, value: &[u8]) {
let pad = (4 - (value.len() % 4)) % 4;
msg.extend_from_slice(&attr_type.to_be_bytes());
msg.extend_from_slice(&(value.len() as u16).to_be_bytes());
msg.extend_from_slice(value);
msg.extend(std::iter::repeat(0u8).take(pad));
let added = (4 + value.len() + pad) as u16;
let new_len = u16::from_be_bytes([msg[2], msg[3]]) + added;
msg[2..4].copy_from_slice(&new_len.to_be_bytes());
}
fn source_a() -> SocketAddr {
"203.0.113.7:51000".parse().unwrap()
}
fn source_b() -> SocketAddr {
"203.0.113.8:51000".parse().unwrap()
}
fn wire_nonce(raw: &[u8; NONCE_LEN]) -> Vec<u8> {
let txid = [9u8; 12];
let msg = encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(raw));
parse_challenge(&msg, &txid).unwrap().nonce.unwrap()
}
#[test]
fn independent_hmac_matches_the_issued_nonce() {
let secret: [u8; 32] = (0u8..32).collect::<Vec<_>>().try_into().unwrap();
let issuer = NonceIssuer::from_secret(secret);
let now: u64 = 1_000_000_000;
let got = issuer.issue(source_a(), now);
let want = hex_to_bytes("00 fe 50 2a e6 b3 5c 05 5e 6b 3d 13 7d 96 f7 c8 c6 97 6a 79");
assert_eq!(
got.to_vec(),
want,
"the HMAC preimage must byte-match an independent computation"
);
}
#[test]
fn issue_then_check_is_fresh_in_the_same_bucket() {
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
assert_eq!(
issuer.check(&wire_nonce(&raw), source_a(), now),
NonceCheck::Fresh
);
}
#[test]
fn issue_then_check_is_fresh_in_the_next_bucket() {
use dig_stun::credential::NONCE_BUCKET_SECS;
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
let wire = wire_nonce(&raw);
assert_eq!(
issuer.check(&wire, source_a(), now + NONCE_BUCKET_SECS),
NonceCheck::Fresh,
"a nonce is valid for its own bucket AND the one after"
);
}
#[test]
fn issue_then_check_is_stale_two_buckets_later() {
use dig_stun::credential::NONCE_BUCKET_SECS;
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
let wire = wire_nonce(&raw);
assert_eq!(
issuer.check(&wire, source_a(), now + 2 * NONCE_BUCKET_SECS),
NonceCheck::Stale
);
}
#[test]
fn check_rejects_a_different_source_ip() {
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
let wire = wire_nonce(&raw);
assert_eq!(issuer.check(&wire, source_b(), now), NonceCheck::Invalid);
}
#[test]
fn check_rejects_a_different_source_port() {
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
let wire = wire_nonce(&raw);
let different_port: SocketAddr = "203.0.113.7:51001".parse().unwrap();
assert_eq!(
issuer.check(&wire, different_port, now),
NonceCheck::Invalid
);
}
#[test]
fn check_rejects_a_different_secret() {
let issuer_a = NonceIssuer::from_secret([1u8; 32]);
let issuer_b = NonceIssuer::from_secret([2u8; 32]);
let now = 1_000_000_000u64;
let raw = issuer_a.issue(source_a(), now);
let wire = wire_nonce(&raw);
assert_eq!(issuer_b.check(&wire, source_a(), now), NonceCheck::Invalid);
}
#[test]
fn check_rejects_a_flipped_byte() {
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
let mut wire = wire_nonce(&raw);
wire[0] = if wire[0] == b'A' { b'B' } else { b'A' };
assert_eq!(issuer.check(&wire, source_a(), now), NonceCheck::Invalid);
}
#[test]
fn check_rejects_a_well_formed_nonce_of_the_wrong_length() {
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let raw = issuer.issue(source_a(), now);
let mut wire = wire_nonce(&raw);
wire.pop(); assert_eq!(issuer.check(&wire, source_a(), now), NonceCheck::Invalid);
}
#[test]
fn ipv4_mapped_and_native_ipv4_source_yield_the_same_nonce() {
let issuer = NonceIssuer::new_random();
let now = 1_000_000_000u64;
let native: SocketAddr = "203.0.113.7:51000".parse().unwrap();
let mapped = SocketAddr::new(
IpAddr::V6(Ipv4Addr::new(203, 0, 113, 7).to_ipv6_mapped()),
51000,
);
assert_eq!(issuer.issue(native, now), issuer.issue(mapped, now));
}
const DUMMY: &[u8] = b"unused-by-decide";
#[test]
fn row1_bare_advisory_answers_with_no_identity() {
let decision = decide(CredentialMode::Advisory, &RequestKind::Bare, None, None);
assert_eq!(decision, ServerDecision::Answer { identity: None });
}
#[test]
fn row2_bare_required_refuses_401_with_no_nonce() {
let decision = decide(CredentialMode::Required, &RequestKind::Bare, None, None);
assert_eq!(
decision,
ServerDecision::Refuse {
code: ERR_UNAUTHENTICATED
}
);
}
#[test]
fn row3_identity_is_challenged_in_both_modes() {
let kind = RequestKind::Identity { spki: DUMMY };
for mode in [CredentialMode::Advisory, CredentialMode::Required] {
assert_eq!(
decide(mode, &kind, None, None),
ServerDecision::Challenge {
code: ERR_UNAUTHENTICATED
}
);
}
}
#[test]
fn row4_signed_invalid_nonce_is_challenged_in_both_modes() {
let kind = RequestKind::Signed {
spki: DUMMY,
nonce: DUMMY,
signature: DUMMY,
};
for mode in [CredentialMode::Advisory, CredentialMode::Required] {
assert_eq!(
decide(mode, &kind, Some(NonceCheck::Invalid), None),
ServerDecision::Challenge {
code: ERR_UNAUTHENTICATED
}
);
}
}
#[test]
fn row5_signed_stale_nonce_is_challenged_438_in_both_modes_never_answer() {
let kind = RequestKind::Signed {
spki: DUMMY,
nonce: DUMMY,
signature: DUMMY,
};
for mode in [CredentialMode::Advisory, CredentialMode::Required] {
let decision = decide(mode, &kind, Some(NonceCheck::Stale), None);
assert_eq!(
decision,
ServerDecision::Challenge {
code: ERR_STALE_NONCE
}
);
assert!(
!matches!(decision, ServerDecision::Answer { .. }),
"stale must never answer"
);
}
}
#[test]
fn row6_signed_fresh_bad_signature_is_challenged_401_in_both_modes() {
let kind = RequestKind::Signed {
spki: DUMMY,
nonce: DUMMY,
signature: DUMMY,
};
for mode in [CredentialMode::Advisory, CredentialMode::Required] {
let decision = decide(
mode,
&kind,
Some(NonceCheck::Fresh),
Some(Err(CredentialError::BadSignature)),
);
assert_eq!(
decision,
ServerDecision::Challenge {
code: ERR_UNAUTHENTICATED
}
);
}
}
#[test]
fn row7_signed_fresh_ok_signature_answers_with_the_verified_identity_in_both_modes() {
let (signer, spki) = ring_test_signer();
let nonce = independent_nonce_wire();
let msg = encode_signed_request(&TXID, nonce, &signer);
let (_, kind) = classify_request(&msg).unwrap();
let identity = verify_signed_request(&TXID, &kind).unwrap();
for mode in [CredentialMode::Advisory, CredentialMode::Required] {
let decision = decide(mode, &kind, Some(NonceCheck::Fresh), Some(Ok(identity)));
match decision {
ServerDecision::Answer {
identity: Some(got),
} => {
assert_eq!(got.spki_der().as_slice(), spki.as_slice());
}
other => panic!("expected Answer{{identity: Some}}, got {other:?}"),
}
}
}
#[test]
fn row8_malformed_is_decided_by_the_caller_directly_from_classify_requests_err() {
let (signer, _spki) = ring_test_signer();
let mut msg = encode_signed_request(&TXID, independent_nonce_wire(), &signer);
append_extra_attribute(&mut msg, 0x9999, &[0, 0, 0, 0]);
let err = classify_request(&msg).unwrap_err();
assert_eq!(err, CredentialError::Malformed);
let response = encode_challenge(&TXID, ERR_BAD_REQUEST, None);
assert_eq!(response.len(), 40);
}
fn reference_server_decide(
mode: CredentialMode,
kind: &RequestKind<'_>,
nonce_check: Option<NonceCheck>,
txid: &TransactionId,
verify_calls: &AtomicUsize,
) -> ServerDecision {
let verified = match (kind, nonce_check) {
(RequestKind::Signed { .. }, Some(NonceCheck::Fresh)) => {
verify_calls.fetch_add(1, Ordering::SeqCst);
Some(verify_signed_request(txid, kind))
}
_ => None,
};
decide(mode, kind, nonce_check, verified)
}
#[test]
fn verify_signed_request_is_not_invoked_for_stale_or_invalid_nonce_only_for_fresh() {
let counter = AtomicUsize::new(0);
let kind = RequestKind::Signed {
spki: DUMMY,
nonce: DUMMY,
signature: DUMMY,
};
reference_server_decide(
CredentialMode::Advisory,
&kind,
Some(NonceCheck::Invalid),
&TXID,
&counter,
);
assert_eq!(
counter.load(Ordering::SeqCst),
0,
"an Invalid nonce must never reach verify_signed_request"
);
reference_server_decide(
CredentialMode::Advisory,
&kind,
Some(NonceCheck::Stale),
&TXID,
&counter,
);
assert_eq!(
counter.load(Ordering::SeqCst),
0,
"a Stale nonce must never reach verify_signed_request"
);
let (signer, _spki) = ring_test_signer();
let msg = encode_signed_request(&TXID, independent_nonce_wire(), &signer);
let (_, fresh_kind) = classify_request(&msg).unwrap();
reference_server_decide(
CredentialMode::Advisory,
&fresh_kind,
Some(NonceCheck::Fresh),
&TXID,
&counter,
);
assert_eq!(
counter.load(Ordering::SeqCst),
1,
"a Fresh nonce must reach verify_signed_request exactly once"
);
}
struct RingTestSigner {
key_pair: ring::signature::EcdsaKeyPair,
spki: Vec<u8>,
}
impl StunSigner for RingTestSigner {
fn spki_der(&self) -> &[u8] {
&self.spki
}
fn sign(&self, message: &[u8]) -> Vec<u8> {
let rng = ring::rand::SystemRandom::new();
self.key_pair
.sign(&rng, message)
.expect("ring signing must succeed")
.as_ref()
.to_vec()
}
}
fn ring_test_signer() -> (RingTestSigner, Vec<u8>) {
let rng = ring::rand::SystemRandom::new();
let pkcs8 = ring::signature::EcdsaKeyPair::generate_pkcs8(
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
&rng,
)
.expect("ring key generation must succeed");
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(
&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
pkcs8.as_ref(),
&rng,
)
.expect("ring must load its own freshly generated pkcs8");
let point = key_pair.public_key().as_ref().to_vec(); let mut spki = P256_SPKI_PREFIX.to_vec();
spki.extend_from_slice(&point);
let spki_clone = spki.clone();
(RingTestSigner { key_pair, spki }, spki_clone)
}
async fn bind_loopback() -> (UdpSocket, SocketAddr) {
let socket = UdpSocket::bind("127.0.0.1:0")
.await
.expect("bind an ephemeral loopback UDP socket");
let addr = socket.local_addr().expect("bound socket has a local addr");
(socket, addr)
}
fn a_globally_routable_reflexive_addr() -> SocketAddr {
"1.1.1.1:51000".parse().unwrap()
}
fn now_unix() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
#[tokio::test]
async fn bare_success_on_first_request_is_accepted_old_server() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let reflexive = a_globally_routable_reflexive_addr();
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let txid = parse_binding_request(&buf[..n]).unwrap();
let resp = dig_stun::encode_binding_success(&txid, reflexive);
server.send_to(&resp, from).await.unwrap();
});
let got = query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer)
.await
.expect("an old server's bare success must be accepted immediately");
assert_eq!(got, reflexive);
server_task.await.unwrap();
}
#[tokio::test]
async fn challenge_then_signed_then_success() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let reflexive = a_globally_routable_reflexive_addr();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, kind) = classify_request(&buf[..n]).unwrap();
assert!(matches!(kind, RequestKind::Identity { .. }));
let nonce = issuer.issue(from, now_unix());
let reply = encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&nonce));
server.send_to(&reply, from).await.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, kind) = classify_request(&buf[..n]).unwrap();
let RequestKind::Signed {
nonce: got_nonce, ..
} = kind
else {
panic!("expected Signed");
};
assert_eq!(issuer.check(got_nonce, from, now_unix()), NonceCheck::Fresh);
verify_signed_request(&txid, &kind).expect("a genuine ring signature must verify");
let reply = dig_stun::encode_binding_success(&txid, reflexive);
server.send_to(&reply, from).await.unwrap();
});
let got = query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer)
.await
.expect("challenge -> signed -> success must succeed");
assert_eq!(got, reflexive);
server_task.await.unwrap();
}
#[tokio::test]
async fn stale_once_then_resigned_then_success() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let reflexive = a_globally_routable_reflexive_addr();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let ancient = now_unix() - 10_000; let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _kind) = classify_request(&buf[..n]).unwrap();
let stale_nonce = issuer.issue(from, ancient);
server
.send_to(
&encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&stale_nonce)),
from,
)
.await
.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, kind) = classify_request(&buf[..n]).unwrap();
let RequestKind::Signed { nonce, .. } = kind else {
panic!("expected Signed")
};
assert_eq!(issuer.check(nonce, from, now_unix()), NonceCheck::Stale);
let fresh_nonce = issuer.issue(from, now_unix());
server
.send_to(
&encode_challenge(&txid, ERR_STALE_NONCE, Some(&fresh_nonce)),
from,
)
.await
.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, kind) = classify_request(&buf[..n]).unwrap();
let RequestKind::Signed { nonce, .. } = kind else {
panic!("expected Signed")
};
assert_eq!(issuer.check(nonce, from, now_unix()), NonceCheck::Fresh);
verify_signed_request(&txid, &kind).expect("the re-signed request must verify");
server
.send_to(&dig_stun::encode_binding_success(&txid, reflexive), from)
.await
.unwrap();
});
let got = query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer)
.await
.expect("438 once -> re-signed -> success must succeed");
assert_eq!(got, reflexive);
server_task.await.unwrap();
}
#[tokio::test]
async fn stale_twice_is_refused_no_fourth_datagram_sent() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let ancient = now_unix() - 10_000;
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap(); let (txid, _) = classify_request(&buf[..n]).unwrap();
let n1 = issuer.issue(from, ancient);
server
.send_to(
&encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&n1)),
from,
)
.await
.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap(); let (txid, _) = classify_request(&buf[..n]).unwrap();
let n2 = issuer.issue(from, ancient);
server
.send_to(&encode_challenge(&txid, ERR_STALE_NONCE, Some(&n2)), from)
.await
.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap(); let (txid, _) = classify_request(&buf[..n]).unwrap();
let n3 = issuer.issue(from, ancient);
server
.send_to(&encode_challenge(&txid, ERR_STALE_NONCE, Some(&n3)), from)
.await
.unwrap();
let fourth =
tokio::time::timeout(Duration::from_millis(300), server.recv_from(&mut buf)).await;
assert!(
fourth.is_err(),
"the client must not send a fourth datagram after a second 438"
);
});
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer).await;
assert_eq!(
result,
Err(SignedQueryError::Refused {
code: ERR_STALE_NONCE
})
);
server_task.await.unwrap();
}
#[tokio::test]
async fn foreign_realm_401_is_refused_with_no_second_request_sent() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let nonce = issuer.issue(from, now_unix());
let mut reply = encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&nonce));
let realm_offset = reply.len() - 4 - 27 ;
let _ = realm_offset; let needle = b"dig-stun";
let pos = reply
.windows(needle.len())
.position(|w| w == needle)
.expect("the challenge must carry the REALM value verbatim");
reply[pos..pos + needle.len()].copy_from_slice(b"not-dig!");
server.send_to(&reply, from).await.unwrap();
let second =
tokio::time::timeout(Duration::from_millis(300), server.recv_from(&mut buf)).await;
assert!(
second.is_err(),
"a foreign-realm 401 must not provoke a second request"
);
});
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer).await;
assert_eq!(
result,
Err(SignedQueryError::Refused {
code: ERR_UNAUTHENTICATED
})
);
server_task.await.unwrap();
}
#[tokio::test]
async fn mismatched_transaction_id_is_ignored_and_the_wait_continues() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let reflexive = a_globally_routable_reflexive_addr();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let wrong_txid = {
let mut t = txid;
t[0] ^= 0xff;
t
};
let nonce = issuer.issue(from, now_unix());
let decoy = encode_challenge(&wrong_txid, ERR_UNAUTHENTICATED, Some(&nonce));
server.send_to(&decoy, from).await.unwrap();
let genuine = encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&nonce));
server.send_to(&genuine, from).await.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, kind) = classify_request(&buf[..n]).unwrap();
verify_signed_request(&txid, &kind).expect("the exchange must still complete normally");
server
.send_to(&dig_stun::encode_binding_success(&txid, reflexive), from)
.await
.unwrap();
});
let got = query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer)
.await
.expect("a mismatched-txid decoy must not abort the exchange");
assert_eq!(got, reflexive);
server_task.await.unwrap();
}
#[tokio::test]
async fn the_whole_exchange_respects_one_timeout() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let timeout = Duration::from_millis(200);
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let nonce = issuer.issue(from, now_unix());
server
.send_to(
&encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&nonce)),
from,
)
.await
.unwrap();
let _ = server.recv_from(&mut buf).await;
});
let started = std::time::Instant::now();
let result = query_reflexive_address_signed(&client, server_addr, timeout, &signer).await;
let elapsed = started.elapsed();
assert_eq!(result, Err(SignedQueryError::Stun(StunError::Timeout)));
assert!(
elapsed < timeout + Duration::from_millis(150),
"elapsed {elapsed:?} suggests a fresh timeout was granted per step instead of one shared deadline"
);
server_task.await.unwrap();
}
#[tokio::test]
async fn bad_challenge_when_the_signers_own_spki_is_invalid_nothing_is_sent() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
struct BrokenSigner;
impl StunSigner for BrokenSigner {
fn spki_der(&self) -> &[u8] {
b"too-short"
}
fn sign(&self, _message: &[u8]) -> Vec<u8> {
panic!("must never be called: the shape check happens before any send")
}
}
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 512];
let nothing =
tokio::time::timeout(Duration::from_millis(300), server.recv_from(&mut buf)).await;
assert!(
nothing.is_err(),
"a signer with an invalid SPKI must never send anything"
);
});
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &BrokenSigner)
.await;
assert_eq!(result, Err(SignedQueryError::BadChallenge));
server_task.await.unwrap();
}
#[tokio::test]
async fn a_stray_reply_from_a_different_source_is_ignored() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (attacker, _attacker_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let reflexive = a_globally_routable_reflexive_addr();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let mut buf = [0u8; 512];
let (n, client_addr) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let decoy = dig_stun::encode_binding_success(&txid, reflexive);
attacker.send_to(&decoy, client_addr).await.unwrap();
let nonce = issuer.issue(client_addr, now_unix());
server
.send_to(
&encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&nonce)),
client_addr,
)
.await
.unwrap();
let (n, client_addr) = server.recv_from(&mut buf).await.unwrap();
let (txid, kind) = classify_request(&buf[..n]).unwrap();
verify_signed_request(&txid, &kind).expect("the genuine signed request must verify");
server
.send_to(
&dig_stun::encode_binding_success(&txid, reflexive),
client_addr,
)
.await
.unwrap();
});
let got = query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer)
.await
.expect("a stray decoy from another socket must not short-circuit the exchange");
assert_eq!(got, reflexive);
server_task.await.unwrap();
}
#[tokio::test]
async fn a_never_dialable_success_address_is_rejected() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let bogus: SocketAddr = "127.0.0.1:1234".parse().unwrap();
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
server
.send_to(&dig_stun::encode_binding_success(&txid, bogus), from)
.await
.unwrap();
});
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer).await;
assert_eq!(
result,
Err(SignedQueryError::Stun(StunError::NoMappedAddress))
);
server_task.await.unwrap();
}
#[tokio::test]
async fn a_success_with_a_mismatched_transaction_id_is_ignored() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let reflexive = a_globally_routable_reflexive_addr();
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let wrong_txid = {
let mut t = txid;
t[0] ^= 0xff;
t
};
server
.send_to(
&dig_stun::encode_binding_success(&wrong_txid, reflexive),
from,
)
.await
.unwrap();
server
.send_to(&dig_stun::encode_binding_success(&txid, reflexive), from)
.await
.unwrap();
});
let got = query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer)
.await
.expect("a mismatched-txid success decoy must not abort the exchange");
assert_eq!(got, reflexive);
server_task.await.unwrap();
}
#[tokio::test]
async fn after_first_challenge_any_other_response_is_refused_not_a_third_datagram() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let server_task = tokio::spawn(async move {
let issuer = NonceIssuer::new_random();
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let nonce = issuer.issue(from, now_unix());
server
.send_to(
&encode_challenge(&txid, ERR_UNAUTHENTICATED, Some(&nonce)),
from,
)
.await
.unwrap();
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
server
.send_to(&encode_challenge(&txid, ERR_BAD_REQUEST, None), from)
.await
.unwrap();
let third =
tokio::time::timeout(Duration::from_millis(300), server.recv_from(&mut buf)).await;
assert!(
third.is_err(),
"an unexpected response after the first challenge must not provoke a third datagram"
);
});
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer).await;
assert_eq!(
result,
Err(SignedQueryError::Refused {
code: ERR_BAD_REQUEST
})
);
server_task.await.unwrap();
}
#[tokio::test]
async fn an_unrecognized_message_type_ends_the_exchange() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 512];
let (n, from) = server.recv_from(&mut buf).await.unwrap();
let (txid, _) = classify_request(&buf[..n]).unwrap();
let mut msg = vec![0x00, 0x11, 0x00, 0x00];
msg.extend_from_slice(&dig_stun::MAGIC_COOKIE.to_be_bytes());
msg.extend_from_slice(&txid);
server.send_to(&msg, from).await.unwrap();
});
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer).await;
assert_eq!(
result,
Err(SignedQueryError::Stun(StunError::UnexpectedType(0x0011)))
);
server_task.await.unwrap();
}
#[tokio::test]
async fn a_truncated_datagram_from_the_server_ends_the_exchange() {
let (client, _client_addr) = bind_loopback().await;
let (server, server_addr) = bind_loopback().await;
let (signer, _spki) = ring_test_signer();
let server_task = tokio::spawn(async move {
let mut buf = [0u8; 512];
let (_n, from) = server.recv_from(&mut buf).await.unwrap();
server.send_to(&[0x01], from).await.unwrap(); });
let result =
query_reflexive_address_signed(&client, server_addr, Duration::from_secs(2), &signer).await;
assert_eq!(result, Err(SignedQueryError::Stun(StunError::Truncated)));
server_task.await.unwrap();
}