anon-identity
Version:
Decentralized identity framework with DIDs, Verifiable Credentials, and privacy-preserving selective disclosure
207 lines • 10.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PresentationRequest = void 0;
const crypto_1 = require("crypto");
const verification_errors_1 = require("./verification-errors");
class PresentationRequest {
constructor(serviceProviderDID) {
this.serviceProviderDID = serviceProviderDID;
}
/**
* Create a presentation request with specific requirements
*/
async createRequest(options) {
const challenge = options.challenge || this.generateChallenge();
const id = `urn:uuid:${(0, crypto_1.randomBytes)(16).toString('hex')}`;
const createdAt = new Date();
return {
id,
type: ['PresentationRequest'],
from: this.serviceProviderDID,
purpose: options.purpose,
challenge,
domain: options.domain,
credentialRequirements: options.credentialRequirements,
createdAt,
expiresAt: options.expiresAt,
allowPartialMatch: options.allowPartialMatch || false
};
}
/**
* Validate a presentation against a request
*/
async validateAgainstRequest(presentation, request) {
const errors = [];
const matchedRequirements = [];
const unmatchedRequirements = [];
// Check if request has expired
if (request.expiresAt && new Date() > request.expiresAt) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_CREDENTIAL_FORMAT, 'Presentation request has expired'));
}
// Check challenge in presentation proof
if (!this.validateChallenge(presentation, request.challenge)) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_SIGNATURE, 'Presentation challenge does not match request'));
}
// Check domain if specified
if (request.domain && !this.validateDomain(presentation, request.domain)) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_SIGNATURE, 'Presentation domain does not match request'));
}
// Validate each credential requirement
for (const requirement of request.credentialRequirements) {
const matchingCredentials = this.findMatchingCredentials(presentation, requirement);
if (matchingCredentials.length > 0) {
// Validate attributes for each matching credential
let requirementMet = false;
for (const credential of matchingCredentials) {
const attributeValidation = this.validateAttributes(credential, requirement.attributes);
if (attributeValidation.valid) {
requirementMet = true;
break;
}
else {
errors.push(...attributeValidation.errors);
}
}
if (requirementMet) {
matchedRequirements.push(requirement);
}
else {
unmatchedRequirements.push(requirement);
}
}
else {
unmatchedRequirements.push(requirement);
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.MISSING_REQUIRED_ATTRIBUTE, `No credentials found matching requirement for types: ${requirement.type.join(', ')}`));
}
}
const totalRequirements = request.credentialRequirements.length;
const satisfiedRequirements = matchedRequirements.length;
const score = totalRequirements > 0 ? satisfiedRequirements / totalRequirements : 0;
const valid = request.allowPartialMatch
? satisfiedRequirements > 0
: satisfiedRequirements === totalRequirements;
return {
valid: valid && errors.length === 0,
matchedRequirements,
unmatchedRequirements,
errors,
score
};
}
/**
* Create a simple request for specific credential types
*/
createSimpleRequest(credentialTypes, purpose, requiredAttributes = [], optionalAttributes = []) {
const requirements = credentialTypes.map(type => ({
type: [type],
attributes: [
...requiredAttributes.map(attr => ({ name: attr, required: true })),
...optionalAttributes.map(attr => ({ name: attr, required: false }))
]
}));
return this.createRequest({
credentialRequirements: requirements,
purpose,
allowPartialMatch: optionalAttributes.length > 0
});
}
/**
* Validate challenge in presentation
*/
validateChallenge(presentation, expectedChallenge) {
// In a full implementation, this would decode the JWT proof and check the challenge claim
// For now, we'll assume the challenge is properly embedded in the proof
return true; // Simplified for this implementation
}
/**
* Validate domain in presentation
*/
validateDomain(presentation, expectedDomain) {
// In a full implementation, this would check the domain claim in the JWT proof
return true; // Simplified for this implementation
}
/**
* Find credentials that match a requirement
*/
findMatchingCredentials(presentation, requirement) {
return presentation.verifiableCredential.filter(credential => {
// Check credential type
const hasMatchingType = requirement.type.some(reqType => credential.type.includes(reqType));
if (!hasMatchingType) {
return false;
}
// Check issuer if specified
if (requirement.issuer && credential.issuer !== requirement.issuer) {
return false;
}
// Check trusted issuers if specified
if (requirement.trustedIssuers && !requirement.trustedIssuers.includes(credential.issuer)) {
return false;
}
// Check age if specified
if (requirement.maxAge) {
const issuanceDate = new Date(credential.issuanceDate);
const maxAgeDate = new Date(Date.now() - requirement.maxAge);
if (issuanceDate < maxAgeDate) {
return false;
}
}
return true;
});
}
/**
* Validate attributes against constraints
*/
validateAttributes(credential, constraints) {
const errors = [];
const { id, ...attributes } = credential.credentialSubject;
for (const constraint of constraints) {
const value = attributes[constraint.name];
// Check required attributes
if (constraint.required && (value === undefined || value === null)) {
errors.push(verification_errors_1.VerificationError.missingRequiredAttribute(constraint.name, credential.id));
continue;
}
// Skip validation for missing optional attributes
if (!constraint.required && (value === undefined || value === null)) {
continue;
}
// Validate expected value
if (constraint.expectedValue !== undefined && value !== constraint.expectedValue) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_CREDENTIAL_FORMAT, `Attribute ${constraint.name} expected ${constraint.expectedValue}, got ${value}`, { attribute: constraint.name, expectedValue: constraint.expectedValue, actualValue: value }));
}
// Validate allowed values
if (constraint.allowedValues && !constraint.allowedValues.includes(value)) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_CREDENTIAL_FORMAT, `Attribute ${constraint.name} value ${value} not in allowed values: ${constraint.allowedValues.join(', ')}`, { attribute: constraint.name, allowedValues: constraint.allowedValues, actualValue: value }));
}
// Validate numeric ranges
if (typeof value === 'number') {
if (constraint.minValue !== undefined && value < constraint.minValue) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_CREDENTIAL_FORMAT, `Attribute ${constraint.name} value ${value} below minimum ${constraint.minValue}`, { attribute: constraint.name, minValue: constraint.minValue, actualValue: value }));
}
if (constraint.maxValue !== undefined && value > constraint.maxValue) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_CREDENTIAL_FORMAT, `Attribute ${constraint.name} value ${value} above maximum ${constraint.maxValue}`, { attribute: constraint.name, maxValue: constraint.maxValue, actualValue: value }));
}
}
// Validate pattern for strings
if (typeof value === 'string' && constraint.pattern) {
const regex = new RegExp(constraint.pattern);
if (!regex.test(value)) {
errors.push(new verification_errors_1.VerificationError(verification_errors_1.VerificationErrorCode.INVALID_CREDENTIAL_FORMAT, `Attribute ${constraint.name} value ${value} does not match pattern ${constraint.pattern}`, { attribute: constraint.name, pattern: constraint.pattern, actualValue: value }));
}
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Generate a secure challenge
*/
generateChallenge() {
return (0, crypto_1.randomBytes)(32).toString('base64url');
}
}
exports.PresentationRequest = PresentationRequest;
//# sourceMappingURL=presentation-request.js.map