Skip to main content

heddle_api/
password_owner.rs

1//! Bounds and canonical signing inputs for password-unlocked client owner keys.
2//! These checks do not turn a password proof into owner authorization.
3
4use ed25519_dalek::{Signature, VerifyingKey};
5use prost::Message;
6use sha2::{Digest, Sha256};
7
8use crate::heddle::api::v1alpha2::{
9    AuthenticationChallenge, CompleteAuthenticationRequest, PasswordChallengeMetadata,
10    PasswordChallengeProof, PasswordDeviceAdmission, PasswordOwnerEnvelopeV1, PasswordOwnerSetup,
11    PasswordOwnerSetupAuthorization, PasswordUnlockContinuation, SignedPasswordDeviceAdmission,
12    SignedPasswordOwnerSetupAuthorization,
13};
14
15pub const MAX_PASSWORD_ENVELOPE_BYTES: usize = 4096;
16pub const MAX_PASSWORD_SETUP_BYTES: usize = 4608;
17pub const PASSWORD_ARGON2_MEMORY_KIB: u32 = 65_536;
18pub const PASSWORD_ARGON2_ITERATIONS: u32 = 3;
19pub const PASSWORD_ARGON2_PARALLELISM: u32 = 4;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
22pub enum PasswordOwnerError {
23    #[error("unsupported password owner format or KDF")]
24    Version,
25    #[error("unsupported password Argon2id costs")]
26    KdfCosts,
27    #[error("invalid password owner field length")]
28    Length,
29    #[error("password owner envelope is oversized or noncanonical")]
30    EnvelopeEncoding,
31    #[error("password owner field binding mismatch")]
32    Binding,
33    #[error("invalid password verifier or owner signature")]
34    Signature,
35}
36
37fn cost(memory: u32, iterations: u32, parallelism: u32) -> Result<(), PasswordOwnerError> {
38    if (memory, iterations, parallelism)
39        != (
40            PASSWORD_ARGON2_MEMORY_KIB,
41            PASSWORD_ARGON2_ITERATIONS,
42            PASSWORD_ARGON2_PARALLELISM,
43        )
44    {
45        return Err(PasswordOwnerError::KdfCosts);
46    }
47    Ok(())
48}
49
50fn size(value: &[u8], expected: usize) -> Result<(), PasswordOwnerError> {
51    if value.len() != expected {
52        return Err(PasswordOwnerError::Length);
53    }
54    Ok(())
55}
56
57fn public_key(value: &[u8]) -> Result<VerifyingKey, PasswordOwnerError> {
58    size(value, 32)?;
59    let bytes: &[u8; 32] = value.try_into().map_err(|_| PasswordOwnerError::Length)?;
60    let mut y = *bytes;
61    y[31] &= 0x7f;
62    let mut prime = [0xff; 32];
63    prime[0] = 0xed;
64    prime[31] = 0x7f;
65    for index in (0..32).rev() {
66        if y[index] < prime[index] {
67            break;
68        }
69        if y[index] > prime[index] || index == 0 {
70            return Err(PasswordOwnerError::Signature);
71        }
72    }
73    let key = VerifyingKey::from_bytes(bytes).map_err(|_| PasswordOwnerError::Signature)?;
74    if key.is_weak() {
75        return Err(PasswordOwnerError::Signature);
76    }
77    Ok(key)
78}
79
80fn operation_id(value: &str) -> Result<(), PasswordOwnerError> {
81    if value.is_empty() || value.len() > 128 {
82        return Err(PasswordOwnerError::Length);
83    }
84    Ok(())
85}
86
87/// Reject unsupported versions/costs before a caller allocates Argon2 memory.
88pub fn validate_password_owner_envelope(
89    value: &PasswordOwnerEnvelopeV1,
90) -> Result<(), PasswordOwnerError> {
91    if value.format_version != 1 || value.kdf_id != 1 {
92        return Err(PasswordOwnerError::Version);
93    }
94    cost(value.memory_kib, value.iterations, value.parallelism)?;
95    size(&value.account_uuid, 16)?;
96    if value.account_uuid.iter().all(|byte| *byte == 0) {
97        return Err(PasswordOwnerError::Binding);
98    }
99    public_key(&value.owner_public_key)?;
100    size(&value.owner_id, 32)?;
101    size(&value.wrap_salt, 16)?;
102    size(&value.nonce, 12)?;
103    size(&value.ciphertext_and_tag, 48)?;
104    if value.encoded_len() > MAX_PASSWORD_ENVELOPE_BYTES {
105        return Err(PasswordOwnerError::EnvelopeEncoding);
106    }
107    Ok(())
108}
109
110/// Decode known fields; callers persist the canonical re-encoding, never raw input.
111pub fn decode_password_owner_envelope_canonical(
112    raw: &[u8],
113) -> Result<PasswordOwnerEnvelopeV1, PasswordOwnerError> {
114    if raw.len() > MAX_PASSWORD_ENVELOPE_BYTES {
115        return Err(PasswordOwnerError::EnvelopeEncoding);
116    }
117    let value =
118        PasswordOwnerEnvelopeV1::decode(raw).map_err(|_| PasswordOwnerError::EnvelopeEncoding)?;
119    validate_password_owner_envelope(&value)?;
120    Ok(value)
121}
122
123/// Exact AES-256-GCM associated data; this binds ciphertext to its root and
124/// Argon2id parameters without giving the server a decrypting key.
125pub fn password_owner_wrap_aad(
126    value: &PasswordOwnerEnvelopeV1,
127) -> Result<Vec<u8>, PasswordOwnerError> {
128    validate_password_owner_envelope(value)?;
129    let mut aad = Vec::with_capacity(28 + 4 + 16 + 32 + 32 + 4 * 4 + 16);
130    aad.extend_from_slice(b"heddle-owner-wrap-aad-v1\0");
131    aad.extend_from_slice(&value.format_version.to_be_bytes());
132    aad.extend_from_slice(&value.account_uuid);
133    aad.extend_from_slice(&value.owner_public_key);
134    aad.extend_from_slice(&value.owner_id);
135    aad.extend_from_slice(&value.kdf_id.to_be_bytes());
136    aad.extend_from_slice(&value.memory_kib.to_be_bytes());
137    aad.extend_from_slice(&value.iterations.to_be_bytes());
138    aad.extend_from_slice(&value.parallelism.to_be_bytes());
139    aad.extend_from_slice(&value.wrap_salt);
140    Ok(aad)
141}
142
143fn validate_password_owner_setup_fields(
144    value: &PasswordOwnerSetup,
145) -> Result<(), PasswordOwnerError> {
146    let envelope = value.envelope.as_ref().ok_or(PasswordOwnerError::Length)?;
147    validate_password_owner_envelope(envelope)?;
148    cost(
149        value.auth_memory_kib,
150        value.auth_iterations,
151        value.auth_parallelism,
152    )?;
153    size(&value.auth_salt, 16)?;
154    public_key(&value.auth_verifier_public_key)?;
155    if value.format_version != 1 || value.auth_kdf_id != 1 {
156        return Err(PasswordOwnerError::Version);
157    }
158    if value.auth_salt == envelope.wrap_salt {
159        return Err(PasswordOwnerError::Binding);
160    }
161    if value.encoded_len() > MAX_PASSWORD_SETUP_BYTES {
162        return Err(PasswordOwnerError::EnvelopeEncoding);
163    }
164    Ok(())
165}
166
167pub fn validate_password_owner_setup(value: &PasswordOwnerSetup) -> Result<(), PasswordOwnerError> {
168    validate_password_owner_setup_fields(value)?;
169    size(&value.auth_verifier_possession_signature, 64)
170}
171
172/// Decode known fields, including nested fields; persist the canonical re-encoding.
173pub fn decode_password_owner_setup_canonical(
174    raw: &[u8],
175) -> Result<PasswordOwnerSetup, PasswordOwnerError> {
176    if raw.len() > MAX_PASSWORD_SETUP_BYTES {
177        return Err(PasswordOwnerError::EnvelopeEncoding);
178    }
179    let value =
180        PasswordOwnerSetup::decode(raw).map_err(|_| PasswordOwnerError::EnvelopeEncoding)?;
181    validate_password_owner_setup(&value)?;
182    Ok(value)
183}
184
185/// Bind an envelope to the accepted account and active owner root.
186pub fn validate_password_owner_setup_binding(
187    setup: &PasswordOwnerSetup,
188    account_uuid: &[u8],
189    owner_public_key: &[u8],
190    owner_id: &[u8],
191) -> Result<(), PasswordOwnerError> {
192    validate_password_owner_setup(setup)?;
193    let envelope = setup.envelope.as_ref().ok_or(PasswordOwnerError::Binding)?;
194    public_key(owner_public_key)?;
195    size(account_uuid, 16)?;
196    size(owner_id, 32)?;
197    if envelope.account_uuid != account_uuid
198        || envelope.owner_public_key != owner_public_key
199        || envelope.owner_id != owner_id
200    {
201        return Err(PasswordOwnerError::Binding);
202    }
203    Ok(())
204}
205
206/// At registration hash the challenge with this domain before signing.
207pub fn password_registration_verifier_possession_digest(
208    challenge: &[u8],
209) -> Result<[u8; 32], PasswordOwnerError> {
210    size(challenge, 32)?;
211    let mut hash = Sha256::new();
212    hash.update(b"heddle-password-verifier-possession-v1");
213    hash.update(challenge);
214    Ok(hash.finalize().into())
215}
216
217/// Verify a domain-separated registration digest or setup authorization digest.
218pub fn verify_password_auth_verifier_possession(
219    setup: &PasswordOwnerSetup,
220    digest: &[u8; 32],
221) -> Result<(), PasswordOwnerError> {
222    validate_password_owner_setup(setup)?;
223    let signature: &[u8; 64] = setup
224        .auth_verifier_possession_signature
225        .as_slice()
226        .try_into()
227        .map_err(|_| PasswordOwnerError::Length)?;
228    public_key(&signature[..32])?;
229    public_key(&setup.auth_verifier_public_key)?
230        .verify_strict(digest, &Signature::from_bytes(signature))
231        .map_err(|_| PasswordOwnerError::Signature)
232}
233
234/// Verify signup possession against the registration challenge.
235pub fn verify_password_auth_verifier_registration_possession(
236    setup: &PasswordOwnerSetup,
237    challenge: &[u8],
238) -> Result<(), PasswordOwnerError> {
239    let digest = password_registration_verifier_possession_digest(challenge)?;
240    verify_password_auth_verifier_possession(setup, &digest)
241}
242
243/// Every successful PUT or DELETE advances the lifetime revision, including a tombstone.
244pub fn next_password_envelope_revision(
245    current: Option<u64>,
246    expected: u64,
247) -> Result<u64, PasswordOwnerError> {
248    let actual = current.unwrap_or(0);
249    if expected != actual || (current.is_some() && actual == 0) {
250        return Err(PasswordOwnerError::Binding);
251    }
252    actual.checked_add(1).ok_or(PasswordOwnerError::Binding)
253}
254
255/// The owner attachment nonce is specific to this challenge and continuation.
256pub fn password_mint_attachment_nonce(
257    challenge_nonce: &[u8],
258    continuation_id: &[u8],
259) -> Result<[u8; 32], PasswordOwnerError> {
260    size(challenge_nonce, 32)?;
261    size(continuation_id, 32)?;
262    let mut hash = Sha256::new();
263    hash.update(b"heddle-password-mint-nonce-v1");
264    hash.update(challenge_nonce);
265    hash.update(continuation_id);
266    Ok(hash.finalize().into())
267}
268
269/// Bind password completion fields to the stored challenge and continuation.
270/// The host still checks expiry, current accepted root, one-use state and PoP,
271/// and compares the continuation revision with its stored challenge revision.
272pub fn validate_password_completion_bindings(
273    request: &CompleteAuthenticationRequest,
274    challenge: &AuthenticationChallenge,
275    continuation: &PasswordUnlockContinuation,
276    bound_device_key: &[u8],
277) -> Result<(), PasswordOwnerError> {
278    let password_challenge = challenge
279        .password_challenge
280        .as_ref()
281        .ok_or(PasswordOwnerError::Binding)?;
282    let completion = match request.proof.as_ref() {
283        Some(
284            crate::heddle::api::v1alpha2::complete_authentication_request::Proof::PasswordUnlock(
285                value,
286            ),
287        ) => value,
288        _ => return Err(PasswordOwnerError::Binding),
289    };
290    let admission = completion
291        .owner_admission
292        .as_ref()
293        .and_then(|signed| signed.admission.as_ref())
294        .ok_or(PasswordOwnerError::Binding)?;
295    let attachment = completion
296        .mint_root_attachment
297        .as_ref()
298        .and_then(|signed| signed.attachment.as_ref())
299        .ok_or(PasswordOwnerError::Binding)?;
300    let envelope = continuation
301        .envelope
302        .as_ref()
303        .ok_or(PasswordOwnerError::Binding)?;
304    let credential_expiry = challenge
305        .credential_expires_at
306        .as_ref()
307        .ok_or(PasswordOwnerError::Binding)?;
308    size(bound_device_key, 32)?;
309    if challenge.method != 2
310        || request.challenge.is_none()
311        || challenge.r#ref.is_none()
312        || request.challenge != challenge.r#ref
313        || !request.enroll_device
314        || !request.ephemeral_public_key.is_empty()
315        || completion.continuation_id != continuation.continuation_id
316        || admission.challenge_id != password_challenge.challenge_id
317        || admission.continuation_id != continuation.continuation_id
318        || admission.account_uuid != envelope.account_uuid
319        || admission.caller_device_public_key != request.caller_public_key
320        || admission.caller_device_public_key != bound_device_key
321        || admission.client_operation_id != request.client_operation_id
322        || admission.owner_state_hash != attachment.owner_state_hash
323        || admission.owner_sequence != attachment.owner_sequence
324        || admission.account_uuid != attachment.account_uuid
325        || attachment.mint_root_key.as_ref().is_none_or(|key| {
326            key.public_key != admission.caller_device_public_key || key.algorithm != 1
327        })
328        || attachment.nonce
329            != password_mint_attachment_nonce(
330                &password_challenge.nonce,
331                &continuation.continuation_id,
332            )?
333        || continuation.envelope_revision == 0
334        || attachment.expires_at_unix_seconds > credential_expiry.seconds
335    {
336        return Err(PasswordOwnerError::Binding);
337    }
338    Ok(())
339}
340
341pub fn validate_password_challenge_metadata(
342    value: &PasswordChallengeMetadata,
343) -> Result<(), PasswordOwnerError> {
344    cost(
345        value.auth_memory_kib,
346        value.auth_iterations,
347        value.auth_parallelism,
348    )?;
349    if value.auth_kdf_id != 1 || value.format_version != 1 {
350        return Err(PasswordOwnerError::Version);
351    }
352    size(&value.auth_salt, 16)?;
353    size(&value.challenge_id, 32)?;
354    size(&value.nonce, 32)?;
355    Ok(())
356}
357
358/// The exact bytes hashed for an Ed25519 password-verifier signature.
359pub fn password_challenge_signing_digest(
360    challenge: &PasswordChallengeMetadata,
361    proof: &PasswordChallengeProof,
362    operation: &str,
363    expiry_unix_seconds: i64,
364) -> Result<[u8; 32], PasswordOwnerError> {
365    validate_password_challenge_metadata(challenge)?;
366    operation_id(operation)?;
367    size(&proof.caller_device_public_key, 32)?;
368    if proof.challenge_id != challenge.challenge_id {
369        return Err(PasswordOwnerError::Binding);
370    }
371    let mut canonical = Vec::with_capacity(32 + 32 + 32 + 4 + operation.len() + 8);
372    canonical.extend_from_slice(&challenge.challenge_id);
373    canonical.extend_from_slice(&challenge.nonce);
374    canonical.extend_from_slice(&proof.caller_device_public_key);
375    canonical.extend_from_slice(&(operation.len() as u32).to_be_bytes());
376    canonical.extend_from_slice(operation.as_bytes());
377    canonical.extend_from_slice(&expiry_unix_seconds.to_be_bytes());
378    let mut hash = Sha256::new();
379    hash.update(b"heddle-password-proof-v1");
380    hash.update(canonical);
381    Ok(hash.finalize().into())
382}
383
384/// Verify a public password verifier proof. A successful result permits only
385/// blob delivery and a bound continuation, never an account credential.
386pub fn verify_password_challenge_signature(
387    challenge: &PasswordChallengeMetadata,
388    proof: &PasswordChallengeProof,
389    operation: &str,
390    expiry_unix_seconds: i64,
391    verifier_public_key: &[u8],
392) -> Result<(), PasswordOwnerError> {
393    size(&proof.signature, 64)?;
394    size(verifier_public_key, 32)?;
395    let digest =
396        password_challenge_signing_digest(challenge, proof, operation, expiry_unix_seconds)?;
397    let signature: &[u8; 64] = proof
398        .signature
399        .as_slice()
400        .try_into()
401        .map_err(|_| PasswordOwnerError::Length)?;
402    public_key(&signature[..32])?;
403    public_key(verifier_public_key)?
404        .verify_strict(&digest, &Signature::from_bytes(signature))
405        .map_err(|_| PasswordOwnerError::Signature)
406}
407
408/// Canonical owner-signed admission digest. Verifiers must also compare the
409/// statement with the one-use continuation and independently current owner.
410pub fn password_device_admission_digest(
411    value: &PasswordDeviceAdmission,
412) -> Result<[u8; 32], PasswordOwnerError> {
413    if value.format_version != 1 {
414        return Err(PasswordOwnerError::Version);
415    }
416    operation_id(&value.client_operation_id)?;
417    size(&value.account_uuid, 16)?;
418    if value.account_uuid.iter().all(|byte| *byte == 0) {
419        return Err(PasswordOwnerError::Binding);
420    }
421    size(&value.challenge_id, 32)?;
422    size(&value.continuation_id, 32)?;
423    size(&value.caller_device_public_key, 32)?;
424    size(&value.owner_state_hash, 32)?;
425    let mut hash = Sha256::new();
426    hash.update(b"heddle-password-device-admission-v1");
427    hash.update(value.format_version.to_be_bytes());
428    hash.update(&value.account_uuid);
429    hash.update(&value.challenge_id);
430    hash.update(&value.continuation_id);
431    hash.update(&value.caller_device_public_key);
432    hash.update(&value.owner_state_hash);
433    hash.update(value.owner_sequence.to_be_bytes());
434    hash.update((value.client_operation_id.len() as u32).to_be_bytes());
435    hash.update(value.client_operation_id.as_bytes());
436    Ok(hash.finalize().into())
437}
438
439/// Verify the independently owner-signed admission against the active root.
440/// The host must additionally bind every admission field to its challenge,
441/// continuation, completion and current owner state before enrolling a key.
442pub fn verify_password_device_admission_signature(
443    signed: &SignedPasswordDeviceAdmission,
444    current_owner_public_key: &[u8],
445) -> Result<(), PasswordOwnerError> {
446    let statement = signed
447        .admission
448        .as_ref()
449        .ok_or(PasswordOwnerError::Binding)?;
450    let signature = signed
451        .owner_signature
452        .as_ref()
453        .ok_or(PasswordOwnerError::Signature)?;
454    let digest = password_device_admission_digest(statement)?;
455    verify_owner_signature(&digest, signature, current_owner_public_key)
456}
457
458/// Hash the fixed-order setup fields named in the wire contract. This is not
459/// protobuf serialization, so changing field order or adding unknowns fails.
460pub fn password_owner_setup_digest(
461    value: &PasswordOwnerSetup,
462) -> Result<[u8; 32], PasswordOwnerError> {
463    validate_password_owner_setup_fields(value)?;
464    let envelope = value.envelope.as_ref().ok_or(PasswordOwnerError::Length)?;
465    let mut hash = Sha256::new();
466    hash.update(envelope.format_version.to_be_bytes());
467    hash.update(&envelope.account_uuid);
468    hash.update(&envelope.owner_public_key);
469    hash.update(&envelope.owner_id);
470    hash.update(envelope.kdf_id.to_be_bytes());
471    hash.update(envelope.memory_kib.to_be_bytes());
472    hash.update(envelope.iterations.to_be_bytes());
473    hash.update(envelope.parallelism.to_be_bytes());
474    hash.update(&envelope.wrap_salt);
475    hash.update(&envelope.nonce);
476    hash.update(&envelope.ciphertext_and_tag);
477    hash.update(&value.auth_salt);
478    hash.update(&value.auth_verifier_public_key);
479    hash.update(value.auth_memory_kib.to_be_bytes());
480    hash.update(value.auth_iterations.to_be_bytes());
481    hash.update(value.auth_parallelism.to_be_bytes());
482    hash.update(value.auth_kdf_id.to_be_bytes());
483    hash.update(value.format_version.to_be_bytes());
484    Ok(hash.finalize().into())
485}
486
487/// Digest signed by the active owner for a compare-and-swap PUT or DELETE.
488/// The host must compare the expected revision and owner-state hash atomically.
489pub fn password_owner_setup_authorization_digest(
490    value: &PasswordOwnerSetupAuthorization,
491    setup: Option<&PasswordOwnerSetup>,
492) -> Result<[u8; 32], PasswordOwnerError> {
493    if value.format_version != 1 {
494        return Err(PasswordOwnerError::Version);
495    }
496    operation_id(&value.client_operation_id)?;
497    size(&value.account_uuid, 16)?;
498    size(&value.owner_state_hash, 32)?;
499    size(&value.setup_sha256, 32)?;
500    if value.account_uuid.iter().all(|byte| *byte == 0) {
501        return Err(PasswordOwnerError::Binding);
502    }
503    let expected_digest = match (value.action, setup) {
504        (1, Some(setup)) => {
505            let envelope = setup.envelope.as_ref().ok_or(PasswordOwnerError::Length)?;
506            if envelope.account_uuid != value.account_uuid {
507                return Err(PasswordOwnerError::Binding);
508            }
509            password_owner_setup_digest(setup)?
510        }
511        (2, None) => [0; 32],
512        _ => return Err(PasswordOwnerError::Binding),
513    };
514    if value.setup_sha256 != expected_digest {
515        return Err(PasswordOwnerError::Binding);
516    }
517    let expiry = value
518        .expires_at
519        .as_ref()
520        .ok_or(PasswordOwnerError::Binding)?;
521    if expiry.nanos != 0 || expiry.seconds <= 0 {
522        return Err(PasswordOwnerError::Binding);
523    }
524    let mut hash = Sha256::new();
525    hash.update(b"heddle-password-owner-setup-change-v1");
526    hash.update(value.format_version.to_be_bytes());
527    hash.update(value.action.to_be_bytes());
528    hash.update(&value.account_uuid);
529    hash.update(&value.owner_state_hash);
530    hash.update(value.expected_revision.to_be_bytes());
531    hash.update(&value.setup_sha256);
532    hash.update((value.client_operation_id.len() as u32).to_be_bytes());
533    hash.update(value.client_operation_id.as_bytes());
534    hash.update(expiry.seconds.to_be_bytes());
535    Ok(hash.finalize().into())
536}
537
538/// Check the signed short-lived authorization at the server's verification time.
539pub fn validate_password_owner_setup_authorization_expiry(
540    value: &PasswordOwnerSetupAuthorization,
541    now_unix_seconds: i64,
542) -> Result<(), PasswordOwnerError> {
543    let expiry = value
544        .expires_at
545        .as_ref()
546        .ok_or(PasswordOwnerError::Binding)?;
547    let latest = now_unix_seconds
548        .checked_add(600)
549        .ok_or(PasswordOwnerError::Binding)?;
550    if expiry.nanos != 0 || expiry.seconds <= now_unix_seconds || expiry.seconds > latest {
551        return Err(PasswordOwnerError::Binding);
552    }
553    Ok(())
554}
555
556/// Verify that a setup change has the current owner's signature. Caller PoP
557/// and account authorization remain separate RPC requirements.
558pub fn verify_password_owner_setup_authorization_signature(
559    signed: &SignedPasswordOwnerSetupAuthorization,
560    setup: Option<&PasswordOwnerSetup>,
561    current_owner_public_key: &[u8],
562) -> Result<(), PasswordOwnerError> {
563    let statement = signed
564        .authorization
565        .as_ref()
566        .ok_or(PasswordOwnerError::Binding)?;
567    let signature = signed
568        .owner_signature
569        .as_ref()
570        .ok_or(PasswordOwnerError::Signature)?;
571    let digest = password_owner_setup_authorization_digest(statement, setup)?;
572    if let Some(setup) = setup {
573        let envelope = setup.envelope.as_ref().ok_or(PasswordOwnerError::Binding)?;
574        if envelope.owner_public_key != current_owner_public_key {
575            return Err(PasswordOwnerError::Binding);
576        }
577        verify_password_auth_verifier_possession(setup, &digest)?;
578    }
579    verify_owner_signature(&digest, signature, current_owner_public_key)
580}
581
582fn verify_owner_signature(
583    digest: &[u8; 32],
584    signature: &crate::heddle::api::v1alpha2::AuthorizationSignature,
585    current_owner_public_key: &[u8],
586) -> Result<(), PasswordOwnerError> {
587    size(current_owner_public_key, 32)?;
588    size(&signature.signer_key_id, 32)?;
589    size(&signature.signature, 64)?;
590    let mut key_id = Sha256::new();
591    key_id.update(b"heddle-key-v1");
592    key_id.update(1_u32.to_be_bytes());
593    key_id.update(current_owner_public_key);
594    if signature.signer_key_id != key_id.finalize().as_slice() {
595        return Err(PasswordOwnerError::Signature);
596    }
597    let signature: &[u8; 64] = signature
598        .signature
599        .as_slice()
600        .try_into()
601        .map_err(|_| PasswordOwnerError::Length)?;
602    public_key(&signature[..32])?;
603    public_key(current_owner_public_key)?
604        .verify_strict(digest, &Signature::from_bytes(signature))
605        .map_err(|_| PasswordOwnerError::Signature)
606}