use mkit_core::repo_identity::RepositoryIdentity;
use super::epoch::EpochStatement;
use super::owner::verify_owner;
use super::visibility::VisibilityStatement;
use super::webauthn::WebAuthnBinding;
use super::{
Capability, Grant, GrantError, MAX_CLOCK_LEAD_MS, OwnerScheme, RefFlags, SignedHeader,
VerifierConfig,
};
macro_rules! verified_statement {
($(#[$meta:meta])* $name:ident, $statement:ty) => {
$(#[$meta])*
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct $name {
statement: $statement,
id: [u8; 32],
scheme: OwnerScheme,
webauthn: Option<WebAuthnBinding>,
}
impl $name {
#[must_use]
pub fn statement(&self) -> &$statement {
&self.statement
}
#[must_use]
pub fn id(&self) -> &[u8; 32] {
&self.id
}
#[must_use]
pub fn scheme(&self) -> OwnerScheme {
self.scheme
}
#[must_use]
pub fn relying_party(&self) -> Option<(&str, &str)> {
self.webauthn
.as_ref()
.map(|b| (b.rp_id.as_str(), b.origin.as_str()))
}
}
};
}
verified_statement!(
OwnerVerified,
Grant
);
verified_statement!(
VerifiedEpoch,
EpochStatement
);
verified_statement!(
VerifiedVisibility,
VisibilityStatement
);
#[derive(Clone, Copy, Debug)]
pub struct GrantRequest<'a> {
pub repository: &'a RepositoryIdentity,
pub signer: &'a [u8; 32],
pub capability: Capability,
pub now_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifiedGrant {
grant: Grant,
id: [u8; 32],
scheme: OwnerScheme,
repository: RepositoryIdentity,
signer: [u8; 32],
capability: Capability,
}
impl VerifiedGrant {
#[must_use]
pub fn grant(&self) -> &Grant {
&self.grant
}
#[must_use]
pub fn id(&self) -> &[u8; 32] {
&self.id
}
#[must_use]
pub fn scheme(&self) -> OwnerScheme {
self.scheme
}
#[must_use]
pub fn repository(&self) -> &RepositoryIdentity {
&self.repository
}
#[must_use]
pub fn signer(&self) -> &[u8; 32] {
&self.signer
}
#[must_use]
pub fn capability(&self) -> Capability {
self.capability
}
#[must_use]
pub fn epoch(&self) -> u64 {
self.grant.epoch
}
#[must_use]
pub fn effective_flags(&self, ref_name: &str) -> RefFlags {
if self.capability != Capability::Write {
return RefFlags::EMPTY;
}
self.grant
.ref_scopes
.as_ref()
.map_or(RefFlags::EMPTY, |scopes| scopes.effective_flags(ref_name))
}
}
fn check_window(created_ms: i64, expiry_ms: i64, now_ms: i64) -> Result<(), GrantError> {
if now_ms < 0 || created_ms > now_ms.saturating_add(MAX_CLOCK_LEAD_MS) {
return Err(GrantError::NotYetValid);
}
if now_ms >= expiry_ms {
return Err(GrantError::Expired);
}
Ok(())
}
fn check_audience(cfg: &VerifierConfig, audiences: &[String]) -> Result<(), GrantError> {
if audiences.iter().any(|a| a == cfg.audience()) {
Ok(())
} else {
Err(GrantError::AudienceNotListed)
}
}
pub fn verify_grant_owner(cfg: &VerifierConfig, header: &str) -> Result<OwnerVerified, GrantError> {
let header = SignedHeader::parse(header)?;
let (grant, id) = Grant::parse_with_id(&header.statement)?;
let webauthn = verify_owner(
cfg,
header.scheme,
&header.statement,
&header.blob,
&grant.namespace,
)?;
Ok(OwnerVerified {
statement: grant,
id,
scheme: header.scheme,
webauthn,
})
}
impl OwnerVerified {
pub fn check(
&self,
cfg: &VerifierConfig,
req: &GrantRequest<'_>,
) -> Result<VerifiedGrant, GrantError> {
let g = &self.statement;
if !cfg.schemes().contains(self.scheme) {
return Err(GrantError::SchemeNotAdvertised);
}
if let Some(b) = &self.webauthn
&& !cfg.allows_relying_party(&b.rp_id, &b.origin)
{
return Err(GrantError::OriginNotAllowed);
}
if req.repository.namespace() != Some(&g.namespace) {
return Err(GrantError::NamespaceMismatch);
}
check_audience(cfg, &g.audiences)?;
if !g.scope.covers(&g.namespace, req.repository) {
return Err(GrantError::RepositoryNotInScope);
}
if !g.capabilities.allows(req.capability) {
return Err(GrantError::CapabilityNotGranted);
}
if g.grantee != *req.signer {
return Err(GrantError::GranteeMismatch);
}
check_window(g.created_ms, g.expiry_ms, req.now_ms)?;
Ok(VerifiedGrant {
grant: g.clone(),
id: self.id,
scheme: self.scheme,
repository: req.repository.clone(),
signer: *req.signer,
capability: req.capability,
})
}
}
pub fn verify_for_registration(
cfg: &VerifierConfig,
header: &str,
principal: &[u8; 32],
) -> Result<OwnerVerified, GrantError> {
let verified = verify_grant_owner(cfg, header)?;
check_audience(cfg, &verified.statement.audiences)?;
if verified.statement.grantee != *principal {
return Err(GrantError::GranteeMismatch);
}
Ok(verified)
}
pub fn verify_epoch_statement(
cfg: &VerifierConfig,
header: &str,
now_ms: i64,
) -> Result<VerifiedEpoch, GrantError> {
let header = SignedHeader::parse(header)?;
let statement = EpochStatement::parse(&header.statement)?;
let webauthn = verify_owner(
cfg,
header.scheme,
&header.statement,
&header.blob,
&statement.namespace,
)?;
check_audience(cfg, &statement.audiences)?;
check_window(statement.created_ms, statement.expiry_ms, now_ms)?;
Ok(VerifiedEpoch {
id: mkit_core::hash::hash(&header.statement),
statement,
scheme: header.scheme,
webauthn,
})
}
pub fn verify_visibility_statement(
cfg: &VerifierConfig,
header: &str,
repository: &RepositoryIdentity,
now_ms: i64,
) -> Result<VerifiedVisibility, GrantError> {
let header = SignedHeader::parse(header)?;
let statement = VisibilityStatement::parse(&header.statement)?;
if statement.repository != *repository {
return Err(GrantError::RepositoryMismatch);
}
let namespace = statement
.repository
.namespace()
.ok_or(GrantError::Repository)?;
let webauthn = verify_owner(
cfg,
header.scheme,
&header.statement,
&header.blob,
namespace,
)?;
check_audience(cfg, &statement.audiences)?;
check_window(statement.created_ms, statement.expiry_ms, now_ms)?;
Ok(VerifiedVisibility {
id: mkit_core::hash::hash(&header.statement),
statement,
scheme: header.scheme,
webauthn,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grant_window_is_exclusive_of_expiry() {
assert_eq!(check_window(100, 200, 199), Ok(()));
assert_eq!(check_window(100, 200, 200), Err(GrantError::Expired));
assert_eq!(check_window(100, 200, 201), Err(GrantError::Expired));
assert_eq!(check_window(100, 200, 70), Ok(()));
assert_eq!(
check_window(30_001, 90_000, 0),
Err(GrantError::NotYetValid)
);
assert_eq!(check_window(30_000, 90_000, 0), Ok(()));
assert_eq!(check_window(0, 10, -1), Err(GrantError::NotYetValid));
assert_eq!(check_window(i64::MAX - 1, i64::MAX, i64::MAX - 1), Ok(()));
assert_eq!(
check_window(i64::MAX - 1, i64::MAX, i64::MAX),
Err(GrantError::Expired)
);
}
}