use crate::admission_budget::{AdmissionBudgetError, SessionAdmissionBudget};
use crate::portable::{
ContextualClaims, DecodedPortableClaims, PortableClaimsContext, PortableClaimsError,
PortableEncodingError, VerifiedPortableClaims,
};
use ic_auth_protocol_types::PortableMechanism;
use ic_platform::ed25519::{Scalar, Signature, VerifyingKey};
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PortableSignatureError {
#[error(transparent)]
Budget(#[from] AdmissionBudgetError),
#[error(transparent)]
Encoding(#[from] PortableEncodingError),
#[error(transparent)]
Claims(#[from] PortableClaimsError),
#[error("invalid or weak Ed25519 public key")]
InvalidPublicKey,
#[error("Ed25519 signature must contain exactly 64 bytes")]
MalformedSignature,
#[error("invalid Ed25519 signature")]
InvalidSignature,
}
pub fn verify_ed25519_claims(
budget: &mut SessionAdmissionBudget,
encoded_claims: &[u8],
signature: &[u8],
context: &PortableClaimsContext<'_>,
public_key: &[u8; 32],
) -> Result<VerifiedPortableClaims, PortableSignatureError> {
let prepared = prepare_claims(DecodedPortableClaims::decode(encoded_claims)?, context)?;
let binding = Sha256::digest(public_key).into();
if context.authority.key_binding_id != binding {
return Err(PortableClaimsError::KeyMismatch.into());
}
let key = PreparedIssuerKey::parse(*public_key, binding)?;
verify_prepared(budget, prepared, signature, &key)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedIssuerKey {
bytes: [u8; 32],
binding: [u8; 32],
key: VerifyingKey,
}
impl PreparedIssuerKey {
pub fn new(bytes: [u8; 32]) -> Result<Self, PortableSignatureError> {
Self::parse(bytes, Sha256::digest(bytes).into())
}
fn parse(bytes: [u8; 32], binding: [u8; 32]) -> Result<Self, PortableSignatureError> {
let key = VerifyingKey::from_bytes(&bytes)
.map_err(|_| PortableSignatureError::InvalidPublicKey)?;
if key.is_weak() {
return Err(PortableSignatureError::InvalidPublicKey);
}
Ok(Self {
bytes,
binding,
key,
})
}
pub fn bytes(&self) -> &[u8; 32] {
&self.bytes
}
pub fn binding(&self) -> [u8; 32] {
self.binding
}
}
pub(crate) fn verify_with_key(
budget: &mut SessionAdmissionBudget,
encoded_claims: &[u8],
signature: &[u8],
context: &PortableClaimsContext<'_>,
public_key: &PreparedIssuerKey,
) -> Result<VerifiedPortableClaims, PortableSignatureError> {
verify_prepared(
budget,
prepare_claims(DecodedPortableClaims::decode(encoded_claims)?, context)?,
signature,
public_key,
)
}
fn prepare_claims<'a, 'ctx>(
decoded: DecodedPortableClaims,
context: &'a PortableClaimsContext<'ctx>,
) -> Result<ContextualClaims<'a, 'ctx>, PortableSignatureError> {
if context.authority.mechanism != PortableMechanism::Ed25519 {
return Err(PortableClaimsError::MechanismMismatch.into());
}
Ok(decoded.validate_context(context)?)
}
pub(crate) fn verify_prepared(
budget: &mut SessionAdmissionBudget,
prepared: ContextualClaims<'_, '_>,
signature: &[u8],
public_key: &PreparedIssuerKey,
) -> Result<VerifiedPortableClaims, PortableSignatureError> {
let (
DecodedPortableClaims {
claims,
fingerprint,
..
},
context,
browser_caller,
) = prepared.into_parts();
if context.authority.mechanism != PortableMechanism::Ed25519 {
return Err(PortableClaimsError::MechanismMismatch.into());
}
if context.authority.key_binding_id != public_key.binding {
return Err(PortableClaimsError::KeyMismatch.into());
}
let signature =
Signature::from_slice(signature).map_err(|_| PortableSignatureError::MalformedSignature)?;
if bool::from(Scalar::from_canonical_bytes(*signature.s_bytes()).is_none()) {
return Err(PortableSignatureError::InvalidSignature);
}
budget.consume(context.authority.now_ns)?;
public_key
.key
.verify_strict(&fingerprint, &signature)
.map_err(|_| PortableSignatureError::InvalidSignature)?;
Ok(VerifiedPortableClaims::new(
claims,
fingerprint,
browser_caller,
context,
))
}