use crate::{Error, Result};
use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
use sha2::{Digest, Sha256};
pub const PUBLIC_KEY_LEN: usize = 65;
pub const SIGNATURE_LEN: usize = 64;
pub const DEVICE_ID_LEN: usize = 32;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DeviceId([u8; DEVICE_ID_LEN]);
impl DeviceId {
#[must_use]
pub fn from_public_key(public_key: &DevicePublicKey) -> Self {
let digest = Sha256::digest(public_key.as_sec1_bytes());
let mut bytes = [0_u8; DEVICE_ID_LEN];
bytes.copy_from_slice(&digest);
Self(bytes)
}
#[must_use]
pub const fn from_bytes(bytes: [u8; DEVICE_ID_LEN]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; DEVICE_ID_LEN] {
&self.0
}
#[must_use]
pub fn to_hex(&self) -> String {
let mut out = String::with_capacity(DEVICE_ID_LEN * 2);
for byte in self.0 {
out.push(char::from_digit(u32::from(byte >> 4), 16).unwrap_or('0'));
out.push(char::from_digit(u32::from(byte & 0x0F), 16).unwrap_or('0'));
}
out
}
}
impl core::fmt::Display for DeviceId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.to_hex())
}
}
impl core::fmt::Debug for DeviceId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "DeviceId({})", self.to_hex())
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct DevicePublicKey {
sec1: [u8; PUBLIC_KEY_LEN],
}
impl DevicePublicKey {
pub fn from_sec1_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != PUBLIC_KEY_LEN {
return Err(Error::DevicePublicKeyInvalid {
reason: "length is not a 65-byte SEC1 uncompressed point",
});
}
if bytes[0] != 0x04 {
return Err(Error::DevicePublicKeyInvalid {
reason: "the SEC1 prefix is not 0x04 (uncompressed form)",
});
}
if VerifyingKey::from_sec1_bytes(bytes).is_err() {
return Err(Error::DevicePublicKeyInvalid {
reason: "the point is not on the P-256 curve",
});
}
let mut sec1 = [0_u8; PUBLIC_KEY_LEN];
sec1.copy_from_slice(bytes);
Ok(Self { sec1 })
}
#[must_use]
pub const fn as_sec1_bytes(&self) -> &[u8; PUBLIC_KEY_LEN] {
&self.sec1
}
#[must_use]
pub fn device_id(&self) -> DeviceId {
DeviceId::from_public_key(self)
}
}
impl core::fmt::Debug for DevicePublicKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "DevicePublicKey(")?;
for byte in self.sec1 {
write!(f, "{byte:02x}")?;
}
write!(f, ")")
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct DeviceSignature([u8; SIGNATURE_LEN]);
impl DeviceSignature {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let raw: [u8; SIGNATURE_LEN] =
bytes
.try_into()
.map_err(|_| Error::DeviceSignatureInvalid {
reason: "length is not 64 raw r||s bytes",
})?;
Ok(Self(raw))
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; SIGNATURE_LEN] {
&self.0
}
}
impl core::fmt::Debug for DeviceSignature {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "DeviceSignature(")?;
for byte in self.0 {
write!(f, "{byte:02x}")?;
}
write!(f, ")")
}
}
#[must_use = "the verification result must be checked; discarding it is the same as not verifying"]
pub fn verify(
public_key: &DevicePublicKey,
message: &[u8],
signature: &DeviceSignature,
) -> Result<()> {
let verifying_key =
VerifyingKey::from_sec1_bytes(public_key.as_sec1_bytes()).map_err(|_| {
Error::DeviceSignatureInvalid {
reason: "the public key failed to parse (should not happen: it was validated at construction)",
}
})?;
let signature =
Signature::from_slice(signature.as_bytes()).map_err(|_| Error::DeviceSignatureInvalid {
reason: "the signature is not a valid P-256 r||s",
})?;
verifying_key
.verify(message, &signature)
.map_err(|_| Error::DeviceSignatureInvalid {
reason: "the signature does not match the public key or the message",
})
}
#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::{SigningKey, signature::Signer};
fn test_key() -> (SigningKey, DevicePublicKey) {
let signing = SigningKey::from_slice(&[7_u8; 32]).expect("valid scalar");
let sec1 = signing.verifying_key().to_sec1_point(false);
let public = DevicePublicKey::from_sec1_bytes(sec1.as_bytes()).expect("valid point");
(signing, public)
}
fn sign(signing: &SigningKey, message: &[u8]) -> DeviceSignature {
let sig: Signature = signing.sign(message);
DeviceSignature::from_bytes(&sig.to_bytes()).expect("64 bytes")
}
#[test]
fn valid_signature_verifies() {
let (signing, public) = test_key();
let message = b"\x01Qualia2FA test message";
let signature = sign(&signing, message);
assert!(verify(&public, message, &signature).is_ok());
}
#[test]
fn modified_message_fails() {
let (signing, public) = test_key();
let mut message = *b"hello qualia2fa!";
let signature = sign(&signing, &message);
message[0] ^= 0x01;
assert!(verify(&public, &message, &signature).is_err());
}
#[test]
fn modified_signature_fails() {
let (signing, public) = test_key();
let message = b"hello qualia2fa!";
let signature = sign(&signing, message);
for index in [0_usize, 31, 32, 63] {
let mut raw = *signature.as_bytes();
raw[index] ^= 0x01;
let tampered = DeviceSignature::from_bytes(&raw).expect("still 64 bytes");
assert!(
verify(&public, message, &tampered).is_err(),
"byte {index} was modified but still verified"
);
}
}
#[test]
fn wrong_public_key_fails() {
let (signing, _) = test_key();
let other = SigningKey::from_slice(&[9_u8; 32]).expect("valid scalar");
let other_sec1 = other.verifying_key().to_sec1_point(false);
let other_public =
DevicePublicKey::from_sec1_bytes(other_sec1.as_bytes()).expect("valid point");
let message = b"hello qualia2fa!";
let signature = sign(&signing, message);
assert!(verify(&other_public, message, &signature).is_err());
}
#[test]
fn off_curve_point_is_rejected() {
let mut bytes = [0_u8; PUBLIC_KEY_LEN];
bytes[0] = 0x04;
bytes[32] = 1;
bytes[64] = 1;
assert!(
DevicePublicKey::from_sec1_bytes(&bytes).is_err(),
"an off-curve point was accepted, which enables invalid-curve attacks"
);
}
#[test]
fn malformed_public_key_is_rejected() {
assert!(DevicePublicKey::from_sec1_bytes(&[]).is_err());
assert!(DevicePublicKey::from_sec1_bytes(&[0x04; 64]).is_err());
assert!(DevicePublicKey::from_sec1_bytes(&[0x04; 66]).is_err());
let (_, public) = test_key();
let mut compressed = *public.as_sec1_bytes();
compressed[0] = 0x02;
assert!(
DevicePublicKey::from_sec1_bytes(&compressed).is_err(),
"the compressed form must not be accepted"
);
}
#[test]
fn malformed_signature_is_rejected() {
assert!(DeviceSignature::from_bytes(&[]).is_err());
assert!(DeviceSignature::from_bytes(&[0_u8; 63]).is_err());
assert!(DeviceSignature::from_bytes(&[0_u8; 65]).is_err());
}
#[test]
fn device_id_is_derived_from_public_key() {
let (_, public) = test_key();
let id = public.device_id();
assert_eq!(id, DeviceId::from_public_key(&public));
assert_eq!(id.as_bytes().len(), DEVICE_ID_LEN);
assert_eq!(id.to_hex().len(), DEVICE_ID_LEN * 2);
let (_, other) = {
let signing = SigningKey::from_slice(&[9_u8; 32]).expect("valid scalar");
let sec1 = signing.verifying_key().to_sec1_point(false);
let public = DevicePublicKey::from_sec1_bytes(sec1.as_bytes()).expect("valid point");
(signing, public)
};
assert_ne!(id, other.device_id());
}
#[test]
fn debug_shows_hex() {
let (_, public) = test_key();
let text = format!("{public:?}");
assert!(text.starts_with("DevicePublicKey(04"), "actual: {text}");
assert_eq!(
text.len(),
"DevicePublicKey(".len() + PUBLIC_KEY_LEN * 2 + 1
);
assert_eq!(public.device_id().to_string().len(), DEVICE_ID_LEN * 2);
}
}