Skip to content

Instantly share code, notes, and snippets.

@KunalKumarSwift
Last active January 10, 2025 08:12
Show Gist options
  • Save KunalKumarSwift/43782b59f0c20372aafd8ae4239df642 to your computer and use it in GitHub Desktop.
Save KunalKumarSwift/43782b59f0c20372aafd8ae4239df642 to your computer and use it in GitHub Desktop.
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const cbor = require('cbor');
const app = express();
app.use(bodyParser.json());
// Apple's App Attest root certificate (replace with actual PEM format certificate)
const APPLE_ROOT_CERT_PEM = `
-----BEGIN CERTIFICATE-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END CERTIFICATE-----
`;
// In-memory storage for challenges (use a database in production)
const challenges = new Map();
/**
* Convert a DER-encoded certificate buffer to PEM format.
*/
function convertDERtoPEM(derBuffer) {
const base64Cert = derBuffer.toString('base64');
const pemCert = `-----BEGIN CERTIFICATE-----\n${base64Cert.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----`;
return pemCert;
}
/**
* Verify a certificate against its issuer's public key.
*/
function verifyCertificate(certPEM, issuerPEM) {
const cert = new crypto.X509Certificate(certPEM);
const issuer = new crypto.X509Certificate(issuerPEM);
// Verify that the certificate is signed by the issuer
return cert.verify(issuer.publicKey);
}
/**
* Generate a random challenge and send it to the client.
*/
app.get('/generate-challenge', (req, res) => {
try {
const challenge = crypto.randomBytes(32).toString('base64');
const challengeId = crypto.randomUUID();
challenges.set(challengeId, challenge);
res.status(200).json({ challengeId, challenge });
} catch (error) {
console.error('Error generating challenge:', error);
res.status(500).json({ error: 'Failed to generate challenge' });
}
});
/**
* Verify the attestation object sent by the client.
*/
app.post('/verify-attestation', async (req, res) => {
const { keyId, attestation, challengeId } = req.body;
try {
// Step 1: Retrieve and validate the original challenge
const originalChallenge = challenges.get(challengeId);
if (!originalChallenge) {
return res.status(400).json({ error: 'Invalid or expired challenge' });
}
// Step 2: Decode Base64-encoded attestation object
const attestationBuffer = Buffer.from(attestation, 'base64');
// Step 3: Parse CBOR-encoded attestation object
const decodedAttestation = cbor.decodeAllSync(attestationBuffer)[0];
console.log('Decoded Attestation:', decodedAttestation);
const { fmt, attStmt, authData } = decodedAttestation;
if (fmt !== 'apple-appattest') {
return res.status(400).json({ error: 'Invalid attestation format' });
}
// Step 4: Extract certificates from x5c
const x5c = attStmt.x5c;
if (!x5c || x5c.length < 2) {
return res.status(400).json({ error: 'Invalid certificate chain' });
}
// Convert DER certificates to PEM format
const leafCertPEM = convertDERtoPEM(Buffer.from(x5c[0]));
const intermediateCertPEM = convertDERtoPEM(Buffer.from(x5c[1]));
const appleRootCertPEM = APPLE_ROOT_CERT_PEM;
// Step 5: Verify certificate chain
if (!verifyCertificate(intermediateCertPEM, appleRootCertPEM)) {
return res.status(400).json({ error: 'Invalid intermediate certificate' });
}
if (!verifyCertificate(leafCertPEM, intermediateCertPEM)) {
return res.status(400).json({ error: 'Invalid leaf certificate' });
}
// Step 6: Validate nonce
const clientDataHash = crypto.createHash('sha256').update(originalChallenge).digest();
const compositeData = Buffer.concat([authData, clientDataHash]);
const nonce = crypto.createHash('sha256').update(compositeData).digest();
// Extract nonce from credCert extension OID 1.2.840.113635.100.8.2
const leafCert = new crypto.X509Certificate(leafCertPEM);
const nonceExtensionOID = '1.2.840.113635.100.8.2';
if (!leafCert.extensions[nonceExtensionOID]) {
return res.status(400).json({ error: 'Nonce extension missing' });
}
const nonceExtensionValue = Buffer.from(leafCert.extensions[nonceExtensionOID].value);
if (!nonce.equals(nonceExtensionValue)) {
return res.status(400).json({ error: 'Nonce mismatch' });
}
// Step 7: Verify key identifier matches public key hash
const publicKeyHash = crypto.createHash('sha256').update(leafCert.publicKey.raw).digest('hex');
if (publicKeyHash !== keyId) {
return res.status(400).json({ error: 'Key identifier mismatch' });
}
// Clean up used challenge
challenges.delete(challengeId);
res.status(200).json({ message: 'Attestation verified successfully' });
} catch (error) {
console.error('Error verifying attestation:', error);
res.status(500).json({ error: 'Internal server error', details: error.message });
}
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment