Back to the graphics index.
I wish someone had just posted code like this when I first learned animation, some 8-10 years ago. (Has it been that long?)
Assuming OpenGL conventions (column vertices on the right, matrices multiply from right to left in operation order).
Note: The data needed for skinned animation includes the actual mesh with bone indices/weights, as well as the animation data with bone position/rotation per frame. Typically, you'll get this from a DCC application like 3ds Max, Maya, or perhaps MilkShape.
// Character soft skinning with animation blending, by Jon Watte.
// Free software under the MIT license.
// Assumes some existing utility/math functions and data types.
// Vector3: x,y,z
// Quaternion: x,y,z,w
// Matrix: 16 elements, translation on right, column major
// Matrix::translation(), Matrix::rotation() and normalize(Quaternion)


// definitions

struct Vertex {
  Vector3 pos; // in object space
  Vector3 normal; // should be normalized
  Vector2 texCoord;
  unsigned byte indices[4]; // < numBones
  float weights[4]; // should sum up to 1
};

struct Bone {  // assuming bone positions relative to identity pose in world space
  Vector3 offset;
  Quaternion ori;
};

Vertex *iVertices, *oVertices;
int numVertices;
Matrix *boneMatrix;
Bone **animationFrames;
int numBones;
GLuint buffer;


// set-up
// load iVertices and animationFrames, numVertices and numBones
...
// allocate oVertices and boneMatrix
...
// allocate the vertex buffer object
glGenBuffers(1, &buffer);
glBindBuffer(GL_ELEMENT_ARRAY, buffer);
glBufferData(GL_ELEMENT_ARRAY, 0, 0, GL_DYNAMIC_DRAW);


// to render, based on frames A and B

// derive the pose from keyframe A and B (lerp by factor L)
for (int i = 0; i < numBones; ++i) {
  boneMatrix[i] = 
    Matrix::translation(animationFrames[A][i].offset * (1-L)
      + animationFrames[B][i].offset * L)
    *
    Matrix::rotation(normalize(
      animationFrames[A][i].ori * (1-L)
      + animationFrames[B][i].ori * L));
}

// calculate the vertex position and normal based on 4-bone blending
for (int i = 0; i < numVertices; ++i) {
  oVertices[i] = iVertices[i];
  oVertices[i].pos = 
    iVertices[i].pos * boneMatrix[iVertices[i].indices[0]] * iVertices[i].weights[0]
  + iVertices[i].pos * boneMatrix[iVertices[i].indices[1]] * iVertices[i].weights[1]
  + iVertices[i].pos * boneMatrix[iVertices[i].indices[2]] * iVertices[i].weights[2]
  + iVertices[i].pos * boneMatrix[iVertices[i].indices[3]] * iVertices[i].weights[3];
  // this only works if there is no non-uniform scale in the bones
  oVertices[i].normal = 
    iVertices[i].normal * boneMatrix[iVertices[i].indices[0]] * iVertices[i].weights[0]
  + iVertices[i].normal * boneMatrix[iVertices[i].indices[1]] * iVertices[i].weights[1]
  + iVertices[i].normal * boneMatrix[iVertices[i].indices[2]] * iVertices[i].weights[2]
  + iVertices[i].normal * boneMatrix[iVertices[i].indices[3]] * iVertices[i].weights[3];
}


glBindBuffer(GL_ELEMENT_ARRAY, buffer);
glBufferSubData(GL_ELEMENT_ARRAY, 0, sizeof(oVertices[0])*numVertices, oVertices);
glVertexPointer(3, GL_FLOAT, sizeof(oVertices[0]), 0);
glNormalPointer(GL_FLOAT, sizeof(oVertices[0]), 12);
glTexCoordPointer(2, GL_FLOAT, sizeof(oVertices[0]), 24);
glDrawRangeElements( /* whatever your triangle list is */ );