1#![forbid(unsafe_code)]
13
14use core::{fmt, marker::PhantomData, str::FromStr};
15
16pub const HASH_BYTES: usize = 32;
18
19pub const MESSAGE_BYTES: usize = HASH_BYTES;
21
22pub const WINTERNITZ_W: usize = 16;
24
25pub const CHAIN_STEPS: usize = WINTERNITZ_W - 1;
27
28pub const MESSAGE_DIGITS: usize = 64;
30
31pub const CHECKSUM_DIGITS: usize = 3;
33
34pub const CHAIN_COUNT: usize = MESSAGE_DIGITS + CHECKSUM_DIGITS;
36
37pub const MASK_COUNT: usize = WINTERNITZ_W;
43
44pub const PRF_INPUT_BYTES: usize = 1 + HASH_BYTES + 2;
46
47pub const SIGNATURE_BYTES: usize = CHAIN_COUNT * HASH_BYTES;
49
50pub const PUBLIC_KEY_BYTES: usize = HASH_BYTES * 2;
52
53pub const FUSED_OUTPUT_BYTES: usize = SIGNATURE_BYTES + PUBLIC_KEY_BYTES;
55
56pub const LEGACY_FUSED_PRIVATE_KEY_KECCAK_PERMUTATIONS: usize =
62 1 + MASK_COUNT + CHAIN_COUNT + CHAIN_COUNT + CHAIN_COUNT * CHAIN_STEPS + 16;
63
64pub const LEGACY_FUSED_PRIVATE_SEED_KECCAK_PERMUTATIONS: usize =
68 1 + LEGACY_FUSED_PRIVATE_KEY_KECCAK_PERMUTATIONS;
69
70pub const SHA256_FUSED_PRIVATE_KEY_COMPRESSION_BLOCKS: usize =
77 1 + MASK_COUNT + CHAIN_COUNT + 2 * CHAIN_COUNT + CHAIN_COUNT * CHAIN_STEPS + 34;
78
79pub const SHA256_FUSED_PRIVATE_SEED_COMPRESSION_BLOCKS: usize =
83 1 + SHA256_FUSED_PRIVATE_KEY_COMPRESSION_BLOCKS;
84
85#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
93#[non_exhaustive]
94pub enum ProfileId {
95 LegacyKeccak,
97 HashSigsSha256GenericV1,
99}
100
101impl ProfileId {
102 #[must_use]
109 pub const fn as_str(&self) -> &'static str {
110 match self {
111 Self::LegacyKeccak => "LEGACY_KECCAK",
112 Self::HashSigsSha256GenericV1 => "HASHSIGS_SHA256_GENERIC_V1",
113 }
114 }
115}
116
117impl fmt::Display for ProfileId {
118 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119 formatter.write_str(self.as_str())
120 }
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128#[non_exhaustive]
129pub struct ParseProfileIdError;
130
131impl fmt::Display for ParseProfileIdError {
132 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(
134 formatter,
135 "unrecognized cryptographic profile; expected exactly {} or {}",
136 ProfileId::LegacyKeccak.as_str(),
137 ProfileId::HashSigsSha256GenericV1.as_str()
138 )
139 }
140}
141
142impl std::error::Error for ParseProfileIdError {}
143
144impl TryFrom<&str> for ProfileId {
145 type Error = ParseProfileIdError;
146
147 fn try_from(value: &str) -> Result<Self, Self::Error> {
148 if value == Self::LegacyKeccak.as_str() {
149 Ok(Self::LegacyKeccak)
150 } else if value == Self::HashSigsSha256GenericV1.as_str() {
151 Ok(Self::HashSigsSha256GenericV1)
152 } else {
153 Err(ParseProfileIdError)
154 }
155 }
156}
157
158impl FromStr for ProfileId {
159 type Err = ParseProfileIdError;
160
161 fn from_str(value: &str) -> Result<Self, Self::Err> {
162 Self::try_from(value)
163 }
164}
165
166mod sealed {
167 pub trait Sealed {}
168}
169
170pub trait Profile: sealed::Sealed + 'static {
175 const ID: ProfileId;
177
178 const NAME: &'static str;
183}
184
185#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
190pub struct LegacyKeccak;
191
192impl sealed::Sealed for LegacyKeccak {}
193
194impl Profile for LegacyKeccak {
195 const ID: ProfileId = ProfileId::LegacyKeccak;
196 const NAME: &'static str = Self::ID.as_str();
197}
198
199#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
204pub struct HashSigsSha256GenericV1;
205
206impl sealed::Sealed for HashSigsSha256GenericV1 {}
207
208impl Profile for HashSigsSha256GenericV1 {
209 const ID: ProfileId = ProfileId::HashSigsSha256GenericV1;
210 const NAME: &'static str = Self::ID.as_str();
211}
212
213#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct LengthError {
216 expected: usize,
217 actual: usize,
218}
219
220impl LengthError {
221 #[must_use]
223 pub const fn new(expected: usize, actual: usize) -> Self {
224 Self { expected, actual }
225 }
226
227 #[must_use]
229 pub const fn expected(self) -> usize {
230 self.expected
231 }
232
233 #[must_use]
235 pub const fn actual(self) -> usize {
236 self.actual
237 }
238}
239
240impl fmt::Display for LengthError {
241 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
242 write!(
243 formatter,
244 "expected {} bytes, received {} bytes",
245 self.expected, self.actual
246 )
247 }
248}
249
250impl std::error::Error for LengthError {}
251
252#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
258#[repr(transparent)]
259pub struct MessageDigest([u8; MESSAGE_BYTES]);
260
261impl MessageDigest {
262 #[must_use]
264 pub const fn new(bytes: [u8; MESSAGE_BYTES]) -> Self {
265 Self(bytes)
266 }
267
268 pub fn from_slice(bytes: &[u8]) -> Result<Self, LengthError> {
274 let value: [u8; MESSAGE_BYTES] = bytes
275 .try_into()
276 .map_err(|_| LengthError::new(MESSAGE_BYTES, bytes.len()))?;
277 Ok(Self(value))
278 }
279
280 #[must_use]
282 pub const fn as_bytes(&self) -> &[u8; MESSAGE_BYTES] {
283 &self.0
284 }
285
286 #[must_use]
288 pub const fn into_bytes(self) -> [u8; MESSAGE_BYTES] {
289 self.0
290 }
291}
292
293pub struct PrivateSeed<P: Profile> {
299 bytes: [u8; HASH_BYTES],
300 profile: PhantomData<fn() -> P>,
301}
302
303impl<P: Profile> PrivateSeed<P> {
304 #[must_use]
306 pub const fn new(bytes: [u8; HASH_BYTES]) -> Self {
307 Self {
308 bytes,
309 profile: PhantomData,
310 }
311 }
312
313 pub fn from_slice(bytes: &[u8]) -> Result<Self, LengthError> {
319 let value: [u8; HASH_BYTES] = bytes
320 .try_into()
321 .map_err(|_| LengthError::new(HASH_BYTES, bytes.len()))?;
322 Ok(Self::new(value))
323 }
324
325 #[must_use]
327 pub const fn expose_secret(&self) -> &[u8; HASH_BYTES] {
328 &self.bytes
329 }
330
331 #[must_use]
333 pub const fn into_secret(self) -> [u8; HASH_BYTES] {
334 self.bytes
335 }
336}
337
338impl<P: Profile> fmt::Debug for PrivateSeed<P> {
339 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
340 formatter
341 .debug_struct("PrivateSeed")
342 .field("profile", &P::NAME)
343 .field("bytes", &"[REDACTED]")
344 .finish()
345 }
346}
347
348pub struct PrivateKey<P: Profile> {
355 bytes: [u8; HASH_BYTES],
356 profile: PhantomData<fn() -> P>,
357}
358
359impl<P: Profile> PrivateKey<P> {
360 #[must_use]
362 pub const fn new(bytes: [u8; HASH_BYTES]) -> Self {
363 Self {
364 bytes,
365 profile: PhantomData,
366 }
367 }
368
369 pub fn from_slice(bytes: &[u8]) -> Result<Self, LengthError> {
375 let value: [u8; HASH_BYTES] = bytes
376 .try_into()
377 .map_err(|_| LengthError::new(HASH_BYTES, bytes.len()))?;
378 Ok(Self::new(value))
379 }
380
381 #[must_use]
383 pub const fn expose_secret(&self) -> &[u8; HASH_BYTES] {
384 &self.bytes
385 }
386
387 #[must_use]
389 pub const fn into_secret(self) -> [u8; HASH_BYTES] {
390 self.bytes
391 }
392}
393
394impl<P: Profile> fmt::Debug for PrivateKey<P> {
395 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
396 formatter
397 .debug_struct("PrivateKey")
398 .field("profile", &P::NAME)
399 .field("bytes", &"[REDACTED]")
400 .finish()
401 }
402}
403
404#[derive(Clone, Copy, Eq, Hash, PartialEq)]
409pub struct PublicKey<P: Profile> {
410 public_seed: [u8; HASH_BYTES],
411 endpoint_hash: [u8; HASH_BYTES],
412 profile: PhantomData<fn() -> P>,
413}
414
415impl<P: Profile> PublicKey<P> {
416 #[must_use]
418 pub const fn new(public_seed: [u8; HASH_BYTES], public_key_hash: [u8; HASH_BYTES]) -> Self {
419 Self {
420 public_seed,
421 endpoint_hash: public_key_hash,
422 profile: PhantomData,
423 }
424 }
425
426 pub fn from_slice(bytes: &[u8]) -> Result<Self, LengthError> {
432 if bytes.len() != PUBLIC_KEY_BYTES {
433 return Err(LengthError::new(PUBLIC_KEY_BYTES, bytes.len()));
434 }
435 let mut public_seed = [0_u8; HASH_BYTES];
436 let mut public_key_hash = [0_u8; HASH_BYTES];
437 public_seed.copy_from_slice(&bytes[..HASH_BYTES]);
438 public_key_hash.copy_from_slice(&bytes[HASH_BYTES..]);
439 Ok(Self::new(public_seed, public_key_hash))
440 }
441
442 #[must_use]
444 pub const fn public_seed(&self) -> &[u8; HASH_BYTES] {
445 &self.public_seed
446 }
447
448 #[must_use]
450 pub const fn public_key_hash(&self) -> &[u8; HASH_BYTES] {
451 &self.endpoint_hash
452 }
453
454 #[must_use]
456 pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_BYTES] {
457 let mut bytes = [0_u8; PUBLIC_KEY_BYTES];
458 bytes[..HASH_BYTES].copy_from_slice(&self.public_seed);
459 bytes[HASH_BYTES..].copy_from_slice(&self.endpoint_hash);
460 bytes
461 }
462}
463
464impl<P: Profile> fmt::Debug for PublicKey<P> {
465 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
466 formatter
467 .debug_struct("PublicKey")
468 .field("profile", &P::NAME)
469 .field("public_seed", &self.public_seed)
470 .field("public_key_hash", &self.endpoint_hash)
471 .finish()
472 }
473}
474
475#[derive(Clone, Eq, PartialEq)]
477pub struct Signature<P: Profile> {
478 segments: [[u8; HASH_BYTES]; CHAIN_COUNT],
479 profile: PhantomData<fn() -> P>,
480}
481
482impl<P: Profile> Signature<P> {
483 #[must_use]
485 pub const fn new(segments: [[u8; HASH_BYTES]; CHAIN_COUNT]) -> Self {
486 Self {
487 segments,
488 profile: PhantomData,
489 }
490 }
491
492 pub fn from_slice(bytes: &[u8]) -> Result<Self, LengthError> {
498 if bytes.len() != SIGNATURE_BYTES {
499 return Err(LengthError::new(SIGNATURE_BYTES, bytes.len()));
500 }
501 let mut segments = [[0_u8; HASH_BYTES]; CHAIN_COUNT];
502 for (segment, chunk) in segments.iter_mut().zip(bytes.chunks_exact(HASH_BYTES)) {
503 segment.copy_from_slice(chunk);
504 }
505 Ok(Self::new(segments))
506 }
507
508 #[must_use]
510 pub const fn segments(&self) -> &[[u8; HASH_BYTES]; CHAIN_COUNT] {
511 &self.segments
512 }
513
514 #[must_use]
516 pub fn to_bytes(&self) -> [u8; SIGNATURE_BYTES] {
517 let mut bytes = [0_u8; SIGNATURE_BYTES];
518 for (chunk, segment) in bytes.chunks_exact_mut(HASH_BYTES).zip(self.segments.iter()) {
519 chunk.copy_from_slice(segment);
520 }
521 bytes
522 }
523}
524
525impl<P: Profile> fmt::Debug for Signature<P> {
526 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
527 formatter
528 .debug_struct("Signature")
529 .field("profile", &P::NAME)
530 .field("segments", &self.segments.as_slice())
531 .finish()
532 }
533}
534
535const _: () = {
536 assert!(HASH_BYTES == 32);
537 assert!(WINTERNITZ_W == 16);
538 assert!(MESSAGE_DIGITS == 64);
539 assert!(CHECKSUM_DIGITS == 3);
540 assert!(CHAIN_COUNT == 67);
541 assert!(SIGNATURE_BYTES == 2_144);
542 assert!(PUBLIC_KEY_BYTES == 64);
543 assert!(LEGACY_FUSED_PRIVATE_KEY_KECCAK_PERMUTATIONS == 1_172);
544 assert!(LEGACY_FUSED_PRIVATE_SEED_KECCAK_PERMUTATIONS == 1_173);
545 assert!(SHA256_FUSED_PRIVATE_KEY_COMPRESSION_BLOCKS == 1_257);
546 assert!(SHA256_FUSED_PRIVATE_SEED_COMPRESSION_BLOCKS == 1_258);
547};
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552
553 #[test]
554 fn profile_ids_round_trip_only_through_canonical_text() {
555 for (profile, canonical) in [
556 (ProfileId::LegacyKeccak, "LEGACY_KECCAK"),
557 (
558 ProfileId::HashSigsSha256GenericV1,
559 "HASHSIGS_SHA256_GENERIC_V1",
560 ),
561 ] {
562 assert_eq!(profile.as_str(), canonical);
563 assert_eq!(profile.to_string(), canonical);
564 assert_eq!(canonical.parse::<ProfileId>(), Ok(profile));
565 assert_eq!(ProfileId::try_from(canonical), Ok(profile));
566 }
567 assert_eq!(LegacyKeccak::NAME, LegacyKeccak::ID.as_str());
568 assert_eq!(
569 HashSigsSha256GenericV1::NAME,
570 HashSigsSha256GenericV1::ID.as_str()
571 );
572 }
573
574 #[test]
575 fn profile_id_parser_rejects_every_near_miss() {
576 for noncanonical in [
577 "",
578 "legacy_keccak",
579 "LegacyKeccak",
580 "LEGACY-KECCAK",
581 " LEGACY_KECCAK",
582 "LEGACY_KECCAK ",
583 "LEGACY_KECCAK\n",
584 "hashsigs_sha256_generic_v1",
585 "HASHSIGS-SHA256-GENERIC-V1",
586 "HASHSIGS_SHA256_GENERIC_V1 ",
587 "HASHSIGS_SHA256_GENERIC_V2",
588 ] {
589 assert!(
590 noncanonical.parse::<ProfileId>().is_err(),
591 "{noncanonical:?}"
592 );
593 assert!(
594 ProfileId::try_from(noncanonical).is_err(),
595 "{noncanonical:?}"
596 );
597 }
598
599 let error = "SHA256".parse::<ProfileId>().expect_err("alias must fail");
600 assert_eq!(
601 error.to_string(),
602 concat!(
603 "unrecognized cryptographic profile; expected exactly ",
604 "LEGACY_KECCAK or HASHSIGS_SHA256_GENERIC_V1"
605 )
606 );
607 }
608
609 #[test]
610 fn malformed_lengths_are_rejected() {
611 let error = Signature::<LegacyKeccak>::from_slice(&[0_u8; 31])
612 .expect_err("short signatures must fail");
613 assert_eq!(error.expected(), SIGNATURE_BYTES);
614 assert_eq!(error.actual(), 31);
615 assert!(PublicKey::<LegacyKeccak>::from_slice(&[0_u8; 63]).is_err());
616 assert!(MessageDigest::from_slice(&[0_u8; 33]).is_err());
617 }
618
619 #[test]
620 fn public_key_and_signature_round_trip() {
621 let key = PublicKey::<HashSigsSha256GenericV1>::new([1_u8; 32], [2_u8; 32]);
622 assert_eq!(
623 PublicKey::from_slice(&key.to_bytes()).expect("valid key"),
624 key
625 );
626
627 let signature = Signature::<HashSigsSha256GenericV1>::new([[3_u8; 32]; CHAIN_COUNT]);
628 assert_eq!(
629 Signature::from_slice(&signature.to_bytes()).expect("valid signature"),
630 signature
631 );
632 }
633
634 #[test]
635 fn secret_debug_output_is_redacted() {
636 let key = PrivateKey::<LegacyKeccak>::new([0xA5; 32]);
637 let text = format!("{key:?}");
638 assert!(text.contains("REDACTED"));
639 assert!(!text.contains("165"));
640 }
641}