import { AuthProvider } from '../base-provider.js'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
import { SSHKeyParser } from '../ssh-parser.js'
// Try to import node:crypto (works in Node, not in Bare)
let nodeCrypto
try {
nodeCrypto = await import('node:crypto')
} catch {
nodeCrypto = null
}
/**
* SSHKeyProvider
*
* Authenticates users via SSH public keys:
* - SSH keys must be linked to a user account first
* - Looks for X-SSH-Signature header with signed challenge
* - Used for git operations over HTTP with SSH authentication
*/
export class SSHKeyProvider extends AuthProvider {
constructor(registry) {
super(registry)
this.name = 'ssh-key'
}
/**
* Extract SSH signature from request headers
* @param {Object} req - HTTP request
* @returns {Promise<Object|null>} { publicKey, signature, challenge } or null
*/
async extractCredentials(req) {
const sshSignature = req.headers['x-ssh-signature']
const sshPublicKey = req.headers['x-ssh-publickey']
const sshChallenge = req.headers['x-ssh-challenge']
if (!sshSignature || !sshPublicKey || !sshChallenge) {
return null
}
return {
publicKey: sshPublicKey,
signature: sshSignature,
challenge: sshChallenge
}
}
/**
* Authenticate using SSH key signature
* @param {Object} credentials - { publicKey, signature, challenge, challengeId }
* @returns {Promise<Object>} { identityId, method, verified }
*/
async authenticate(credentials) {
const { publicKey, signature, challenge, challengeId } = credentials
// Calculate fingerprint (same as registry uses)
const fingerprint = this.calculateSSHFingerprint(publicKey)
// Look up identity by SSH key
const identity = await this.registry.getIdentityBySSHKey(publicKey)
if (!identity) {
throw new Error('SSH key not registered. Link your SSH key via web interface first.')
}
// Check if identity is revoked
const isRevoked = await this.registry.isRevoked(identity.identityId)
if (isRevoked) {
throw new Error('SSH key has been revoked')
}
// Verify signature
const isValid = await this.verifySignature(publicKey, signature, challenge)
if (!isValid) {
throw new Error('Invalid SSH signature')
}
return {
identityId: identity.identityId,
method: 'ssh-key',
verified: true,
credentials: { publicKey, fingerprint }
}
}
/**
* Verify SSH signature (RSA or Ed25519)
* @param {string} publicKey - SSH public key
* @param {string} signature - Base64 signature (SSH wire format)
* @param {string} challenge - Challenge string
* @returns {Promise<boolean>} True if valid
*/
async verifySignature(publicKey, signature, challenge) {
try {
// Parse SSH public key
const parsedKey = SSHKeyParser.parse(publicKey)
// Parse SSH signature
const parsedSig = SSHKeyParser.parseSignature(signature)
// Verify key type matches signature type
if (parsedKey.type !== parsedSig.type) {
console.error('[SSH] Key type mismatch:', parsedKey.type, 'vs', parsedSig.type)
return false
}
const challengeBuf = b4a.from(challenge, 'utf8')
// Verify based on key type
if (parsedKey.type === 'rsa') {
return this.verifyRSA(parsedKey, parsedSig.signature, challengeBuf)
} else if (parsedKey.type === 'ed25519') {
return this.verifyEd25519(parsedKey, parsedSig.signature, challengeBuf)
} else {
console.error('[SSH] Unsupported key type:', parsedKey.type)
return false
}
} catch (err) {
console.error('[SSH] Signature verification error:', err)
return false
}
}
/**
* Verify RSA signature (PKCS#1 v1.5 with SHA-256)
* @param {Object} key - Parsed RSA key { e, n }
* @param {Buffer} signature - Raw signature bytes
* @param {Buffer} data - Data that was signed
* @returns {boolean} True if valid
*/
verifyRSA(key, signature, data) {
// Check if node:crypto is available (Node.js environment)
if (!nodeCrypto) {
console.warn('[SSH] RSA verification not available in Bare runtime (use Node.js for full verification)')
// Fall back to basic validation
return this.validateRSASignatureFormat(key, signature)
}
try {
// Convert SSH key format to PEM for node:crypto
const pem = SSHKeyParser.toPEM(key)
// Create verifier
const verifier = nodeCrypto.default.createVerify('RSA-SHA256')
verifier.update(data)
// Verify signature
const isValid = verifier.verify(pem, signature)
if (!isValid) {
console.warn('[SSH] RSA signature verification failed')
}
return isValid
} catch (err) {
console.error('[SSH] RSA verification error:', err)
return false
}
}
/**
* Validate RSA signature format (fallback for Bare runtime)
*
* SECURITY: Cannot verify RSA signatures without node:crypto.
* Always returns false to prevent authentication bypass.
*
* @param {Object} key - Parsed RSA key { e, n }
* @param {Buffer} signature - Raw signature bytes
* @returns {boolean} Always false - RSA auth requires Node.js
*/
validateRSASignatureFormat(key, signature) {
console.error('[SSH] SECURITY: RSA signature verification not available in Bare runtime.')
console.error('[SSH] RSA authentication requires Node.js. Use Ed25519 keys instead.')
return false
}
/**
* Verify Ed25519 signature
* @param {Object} key - Parsed Ed25519 key { publicKey }
* @param {Buffer} signature - Raw signature bytes (64 bytes)
* @param {Buffer} data - Data that was signed
* @returns {boolean} True if valid
*/
verifyEd25519(key, signature, data) {
try {
// Validate sizes
if (key.publicKey.length !== 32) {
console.error('[SSH] Invalid Ed25519 public key size:', key.publicKey.length)
return false
}
if (signature.length !== 64) {
console.error('[SSH] Invalid Ed25519 signature size:', signature.length)
return false
}
// Use hypercore-crypto for Ed25519 verification
const isValid = crypto.verify(data, signature, key.publicKey)
if (!isValid) {
console.warn('[SSH] Ed25519 signature verification failed')
}
return isValid
} catch (err) {
console.error('[SSH] Ed25519 verification error:', err)
return false
}
}
/**
* Calculate SHA256 fingerprint of SSH public key
* @param {string} sshPublicKey - SSH public key (ssh-rsa ...)
* @returns {string} Hex fingerprint
*/
calculateSSHFingerprint(sshPublicKey) {
return crypto.hash(b4a.from(sshPublicKey)).toString('hex').substring(0, 32)
}
/**
* SSH keys cannot be generated by the server
* Users must provide their own SSH public key
*/
async generateIdentity(options = {}) {
throw new Error('SSH keys cannot be generated server-side. Use ssh-keygen to create a key.')
}
}