// Copyright 2024-2026 Tymofii Pidlisnyi. Apache-2.0 license. See LICENSE.
// Agent Attestation Architecture — Core Functions
// Phase 1: Foundation types + issuance challenge + grade computation + attestation binding
import { createHash, randomBytes } from 'crypto';
import { verify } from '../crypto/keys.js';
import { canonicalize } from './canonical.js';
// ── SHA-256 helper ──
function sha256Hex(input) {
    return createHash('sha256').update(input).digest('hex');
}
// ── createIssuanceChallenge ──
// Phase 1 of the 5-phase issuance flow.
// Server generates a nonce bound to the agent's public key.
export function createIssuanceChallenge(publicKeyHash, options) {
    const now = new Date();
    const expiry = new Date(now.getTime() + (options?.expiresInSeconds ?? 300) * 1000);
    return {
        challengeId: `ic_${sha256Hex(randomBytes(32).toString('hex')).slice(0, 24)}`,
        nonce: sha256Hex(randomBytes(32).toString('hex')).slice(0, 32),
        requiredPublicKeyHash: publicKeyHash,
        requestedAttestationClasses: options?.requestedClasses ?? ['runtime'],
        expiresAt: expiry.toISOString(),
        issuedAt: now.toISOString(),
    };
}
// ── verifyRuntimeAttestation ──
// Phase 3: verify an infrastructure attestation.
// Checks: attester signature, nonce binding to challenge, key binding, freshness.
export function verifyRuntimeAttestation(attestation, challenge, trustedAttesterKeys // attester URI -> public key
) {
    const now = new Date();
    // Check freshness
    if (new Date(attestation.expiresAt) < now) {
        return {
            signalKey: 'runtime_attestation',
            status: 'failed',
            detail: 'Runtime attestation has expired',
            verifiedAt: now.toISOString(),
        };
    }
    // Check nonce binding
    if (attestation.nonce !== challenge.nonce) {
        return {
            signalKey: 'runtime_attestation',
            status: 'failed',
            detail: 'Nonce does not match issuance challenge',
            verifiedAt: now.toISOString(),
        };
    }
    // Check key binding
    if (attestation.publicKeyHash !== challenge.requiredPublicKeyHash) {
        return {
            signalKey: 'runtime_attestation',
            status: 'failed',
            detail: 'Public key hash does not match challenge requirement',
            verifiedAt: now.toISOString(),
        };
    }
    // Check attester is trusted
    const attesterKey = trustedAttesterKeys.get(attestation.attester);
    if (!attesterKey) {
        return {
            signalKey: 'runtime_attestation',
            status: 'observed',
            detail: `Attester ${attestation.attester} is not in trusted registry`,
            verifiedAt: now.toISOString(),
        };
    }
    // Verify attester signature
    const payload = canonicalize({
        attester: attestation.attester,
        nonce: attestation.nonce,
        publicKeyHash: attestation.publicKeyHash,
        runtimeClass: attestation.runtimeClass,
        bootEpoch: attestation.bootEpoch,
        runtimeInstanceIdHash: attestation.runtimeInstanceIdHash,
        storageIdentityHash: attestation.storageIdentityHash,
        processIdentityHash: attestation.processIdentityHash,
        issuedAt: attestation.issuedAt,
        expiresAt: attestation.expiresAt,
    });
    const sigValid = verify(payload, attestation.signature, attesterKey);
    if (!sigValid) {
        return {
            signalKey: 'runtime_attestation',
            status: 'failed',
            detail: 'Attester signature verification failed',
            verifiedAt: now.toISOString(),
        };
    }
    return {
        signalKey: 'runtime_attestation',
        status: 'verified',
        detail: `Verified by trusted attester ${attestation.attester}`,
        verifiedAt: now.toISOString(),
    };
}
// ── Evidence-Based Grade Assignment (A2A#1712 — VCOne-AI) ──
// VCOne-AI flagged on A2A#1712: the original grade mapping was method-prefix
// driven. A TPM-backed did:key was Grade 0 because it's did:key; a SPIFFE SVID
// from a misconfigured cluster was Grade 2 because it's SPIFFE. That's backwards.
//
// The fix: classify by EVIDENCE QUALITY, not identity method. A did:key with
// TPM attestation evidence reaches Grade 2, same as a verified SPIFFE SVID.
// Evidence quality determines grade; method is only a fallback signal.
/** Map evidence quality level to passport grade number. */
export function evidenceQualityToGrade(quality) {
    switch (quality) {
        case 'none': return 0;
        case 'issuer_vouched': return 1;
        case 'infrastructure': return 2;
        case 'principal_bound': return 3;
    }
}
// Evidence field keys that indicate infrastructure/hardware binding.
// External attestations come in many shapes — check loosely.
const INFRASTRUCTURE_EVIDENCE_KEYS = [
    'tpm_quote', 'tpmQuote',
    'hardware_attestation', 'hardwareAttestation',
    'tee_proof', 'teeProof',
    'infrastructure_binding', 'infrastructureBinding',
    'sgx_quote', 'sgxQuote',
    'sev_attestation', 'sevAttestation',
    'workload_attestation', 'workloadAttestation',
];
/**
 * Classify evidence quality from attestation metadata.
 *
 * Precedence (highest to lowest):
 *   1. Principal binding → 'principal_bound'
 *   2. Infrastructure evidence (SPIFFE method, TPM quote, hardware attestation,
 *      TEE proof, or any known infrastructure-binding key in evidence) → 'infrastructure'
 *   3. Issuer signature → 'issuer_vouched'
 *   4. None
 *
 * This is where a did:key with TPM evidence gets elevated to Grade 2.
 */
export function classifyEvidenceQuality(opts) {
    // Principal binding takes precedence
    if (opts.hasPrincipalBinding)
        return 'principal_bound';
    // Infrastructure evidence: method-based signal for SPIFFE, plus evidence-key detection
    if (opts.method) {
        const m = opts.method.toLowerCase();
        if (m === 'spiffe' || m.startsWith('spiffe:') || m.startsWith('spiffe://')) {
            return 'infrastructure';
        }
    }
    if (opts.evidence) {
        for (const key of INFRASTRUCTURE_EVIDENCE_KEYS) {
            if (opts.evidence[key] !== undefined && opts.evidence[key] !== null) {
                return 'infrastructure';
            }
        }
    }
    // Issuer vouched
    if (opts.hasIssuerSignature)
        return 'issuer_vouched';
    return 'none';
}
// ── computePassportGrade ──
// Determines grade from available verified attestations.
// Grade 0: self-signed (bare keypair)
// Grade 1: issuer countersigned
// Grade 2: runtime-bound (issuer + challenge-response + trusted attestation)
// Grade 3: runtime + principal bound
export function computePassportGrade(evidence, options) {
    const has = options ?? {};
    // Check from highest to lowest
    if (has.hasVerifiedRuntime && has.hasPrincipalEndorsement && has.hasIssuerSignature) {
        return 3;
    }
    if (has.hasVerifiedRuntime && has.hasIssuerSignature) {
        return 2;
    }
    // Provider attestation without runtime can still be Grade 2
    // if the provider is strong enough (e.g., verified cloud tenant)
    if (has.hasVerifiedProvider && has.hasIssuerSignature) {
        return 2;
    }
    if (has.hasIssuerSignature) {
        return 1;
    }
    return 0;
}
// ── computeAttestationFlags ──
// Derive public flags from evidence and grade.
export function computeAttestationFlags(grade, evidence) {
    const flags = [];
    if (grade >= 1)
        flags.push('issuer_bound');
    if (grade >= 2 && evidence.runtimeAttestations.length > 0)
        flags.push('runtime_bound');
    if (grade >= 2 && evidence.providerAttestations.length > 0)
        flags.push('provider_bound');
    if (grade >= 3)
        flags.push('principal_bound');
    if (evidence.priorPassportRef)
        flags.push('recovery_linked');
    if (evidence.priorContinuityProof)
        flags.push('continuity_proven');
    return flags;
}
// ── computeAttestationBundleHash ──
// SHA-256 of the canonical evidence record. Allows verifiers to confirm
// the passport's attestation summary matches real evidence without seeing the evidence.
export function computeAttestationBundleHash(evidence) {
    return sha256Hex(canonicalize(evidence));
}
// ── createIssuanceContext ──
// Combine evidence + assessment into a complete issuance record.
export function createIssuanceContext(evidence, options) {
    const grade = computePassportGrade(evidence, options);
    const flags = computeAttestationFlags(grade, evidence);
    const bundleHash = computeAttestationBundleHash(evidence);
    return {
        evidence,
        assessment: {
            passportGrade: grade,
            attestationBundleHash: bundleHash,
            flags,
            verificationResults: options?.verificationResults ?? [],
            derivedSignals: options?.derivedSignals,
            assessedAt: new Date().toISOString(),
        },
    };
}
// ── bindAttestation ──
// Attach PassportAttestationSummary to a SignedPassport.
// This is backward compatible: the attestation field is optional.
export function bindAttestation(signedPassport, context) {
    const summary = {
        passportGrade: context.assessment.passportGrade,
        attestationBundleHash: context.assessment.attestationBundleHash,
        flags: context.assessment.flags,
    };
    return {
        ...signedPassport,
        attestation: summary,
    };
}
// ── createWorkspaceManifest ──
// Compute a workspace manifest from file entries.
// Hash structure, not content. Privacy-preserving: paths are hashed,
// timestamps floored to hour.
export function createWorkspaceManifest(entries) {
    const now = new Date();
    // Sort entries deterministically by path hash
    const manifestEntries = entries
        .map(e => {
        const hourFloor = new Date(e.lastModified);
        hourFloor.setMinutes(0, 0, 0);
        return {
            pathHash: sha256Hex(e.path),
            sizeBytes: e.sizeBytes,
            lastModifiedBucket: hourFloor.toISOString(),
        };
    })
        .sort((a, b) => a.pathHash.localeCompare(b.pathHash));
    const totalSize = manifestEntries.reduce((sum, e) => sum + e.sizeBytes, 0);
    const manifestHash = sha256Hex(canonicalize(manifestEntries));
    return {
        entries: manifestEntries,
        totalFiles: manifestEntries.length,
        totalSizeBytes: totalSize,
        computedAt: now.toISOString(),
        manifestHash,
    };
}
// ── createEmptyEvidenceRecord ──
// Initialize a minimal evidence record. The server fills Tier 0 observed signals.
export function createEmptyEvidenceRecord(observed) {
    const now = new Date();
    return {
        requestId: `req_${sha256Hex(Date.now().toString() + Math.random().toString()).slice(0, 24)}`,
        requestedAt: now.toISOString(),
        observed: {
            observedAt: now.toISOString(),
            ...observed,
        },
        runtimeAttestations: [],
        providerAttestations: [],
        selfDeclaredSignals: [],
    };
}
// ── isChallengeFresh ──
// Check if an issuance challenge is still valid.
export function isChallengeFresh(challenge) {
    return new Date(challenge.expiresAt) > new Date();
}
// ── isGradeAtLeast ──
// Check if a passport grade meets a minimum requirement.
export function isGradeAtLeast(grade, minimum) {
    return grade >= minimum;
}
// ── importProviderAttestation ──
// Accept external attestation reports (JWS or raw JSON) and convert to ProviderAttestation.
// Enables composable trust: security test results, cloud attestations, or any third-party
// verification report feeds into the IssuanceEvidenceRecord as Tier 2 evidence.
// Connected to msaleme's machine-readable attestation schema (A2A #1696).
export function importProviderAttestation(input) {
    let payload;
    let signature;
    if (typeof input.attestation === 'string') {
        const parts = input.attestation.split('.');
        if (parts.length === 3) {
            // JWS compact: header.payload.signature
            try {
                const decoded = Buffer.from(parts[1], 'base64url').toString('utf-8');
                payload = JSON.parse(decoded);
                signature = parts[2];
            }
            catch {
                // Not valid JWS, treat as raw JSON
                payload = JSON.parse(input.attestation);
            }
        }
        else {
            payload = JSON.parse(input.attestation);
        }
    }
    else {
        payload = input.attestation;
    }
    // Extract subject identifier and hash it
    const subjectId = String(payload.sub || payload.subject_id || payload.agentId || payload.agent_id || '');
    const subjectIdHash = sha256Hex(subjectId);
    return {
        provider: input.provider,
        subjectClass: input.subjectClass || String(payload.subject_class || 'agent'),
        subjectIdHash,
        nonce: payload.nonce ? String(payload.nonce) : undefined,
        publicKeyHash: payload.public_key ? sha256Hex(String(payload.public_key)) : undefined,
        verificationMethod: input.verificationMethod || String(payload.verification_method || 'jwt'),
        issuedAt: String(payload.iat ? new Date(Number(payload.iat) * 1000).toISOString() : payload.issued_at || new Date().toISOString()),
        expiresAt: payload.exp ? new Date(Number(payload.exp) * 1000).toISOString() : (payload.expires_at ? String(payload.expires_at) : undefined),
        signature,
        freshness: input.freshness,
    };
}
// ── addIdentityBoundary ──
// Add a self-describing identity boundary to any object. The boundary declares which
// fields are included in the content hash, making cross-system attribution possible
// without agreeing on a single hashing standard. Inspired by xsa520's decision artifact spec.
export function addIdentityBoundary(obj, fields) {
    const boundary = (fields || Object.keys(obj)).filter(k => !k.startsWith('_')).sort();
    const hashInput = { _identityBoundary: boundary };
    for (const k of boundary) {
        if (k in obj)
            hashInput[k] = obj[k];
    }
    return {
        ...obj,
        _identityBoundary: boundary,
        _contentHash: sha256Hex(JSON.stringify(hashInput)),
    };
}
//# sourceMappingURL=attestation.js.map