You could change
subroutine knuth_shuffle(array, length)
implicit none
integer :: length
integer :: array(length)
integer :: randomIndex
integer :: temp
to
subroutine knuth_shuffle(array)
implicit none
integer :: array(:)
integer :: length
integer :: randomIndex
integer :: temp
length = size(array)
or you could just chuck away the local variable length and write
subroutine knuth_shuffle(array)
implicit none
integer :: array(:)
integer :: randomIndex
integer :: temp
integer :: i
do i = 1, size(array) - 2
randomIndex = floor(randomBetween(real(i), real(size(array))))
temp = array(i)
array(i) = array(randomIndex)
array(randomIndex) = temp
enddo
end subroutine knuth_shuffle
While I'm answering: the subroutine doesn't know or care if the actual argument supplied as array is allocatable or static. There is nothing in the subroutine that is affected by, nor which affects, the allocation status of the array. From the subroutine's perspective array is of assumed-shape, on which point see this question and answer.
As @francescalus has commented, if it has assumed-shape array arguments the procedure requires an explicit interface The easy way to provide that is to have the compiler generate the interface; either (preferable) put the procedure in a module and use it or (more limited applicability) include it within the contains section of another program unit (maybe the main program unit). A last resort, generally only to be taken when modifying old code, would be to write an interface block yourself.